Add input lock, card actions, combat stats and attack arrow
CI / test (push) Successful in 2m27s
Details
CI / test (push) Successful in 2m27s
Details
This commit is contained in:
parent
309b56e4d7
commit
50487ed3f0
|
|
@ -5,6 +5,7 @@ using UnityEditor.SceneManagement;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.SceneManagement;
|
using UnityEngine.SceneManagement;
|
||||||
using Vanguard.App;
|
using Vanguard.App;
|
||||||
|
using Vanguard.Battle;
|
||||||
using Vanguard.Board3D;
|
using Vanguard.Board3D;
|
||||||
|
|
||||||
namespace Vanguard.Editor
|
namespace Vanguard.Editor
|
||||||
|
|
@ -20,6 +21,8 @@ namespace Vanguard.Editor
|
||||||
private const string BootPath = "Assets/Scenes/Boot.unity";
|
private const string BootPath = "Assets/Scenes/Boot.unity";
|
||||||
private const string PlaymatConfigPath =
|
private const string PlaymatConfigPath =
|
||||||
"Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset";
|
"Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset";
|
||||||
|
private const string AnimationConfigPath =
|
||||||
|
"Assets/Vanguard/Resources/AnimationConfig.asset";
|
||||||
private const string PlaymatScenePath = SceneDir + "/GameBoard3D.unity";
|
private const string PlaymatScenePath = SceneDir + "/GameBoard3D.unity";
|
||||||
|
|
||||||
[MenuItem("Vanguard/Rebuild Client Scenes")]
|
[MenuItem("Vanguard/Rebuild Client Scenes")]
|
||||||
|
|
@ -36,6 +39,7 @@ namespace Vanguard.Editor
|
||||||
Create<DeckBuilderSceneController>("DeckBuilder");
|
Create<DeckBuilderSceneController>("DeckBuilder");
|
||||||
Create<BattleSceneController>("Battle");
|
Create<BattleSceneController>("Battle");
|
||||||
EnsurePlaymatConfig();
|
EnsurePlaymatConfig();
|
||||||
|
EnsureAnimationConfig();
|
||||||
|
|
||||||
var scenes = new List<EditorBuildSettingsScene>();
|
var scenes = new List<EditorBuildSettingsScene>();
|
||||||
Add(scenes, BootPath);
|
Add(scenes, BootPath);
|
||||||
|
|
@ -92,6 +96,35 @@ namespace Vanguard.Editor
|
||||||
Debug.Log("Vanguard playmat sandbox rebuilt: " + PlaymatScenePath);
|
Debug.Log("Vanguard playmat sandbox rebuilt: " + PlaymatScenePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MenuItem("Vanguard/Rebuild Animation Config")]
|
||||||
|
public static void BuildAnimationConfig()
|
||||||
|
{
|
||||||
|
EnsureAnimationConfig();
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
Debug.Log("Vanguard animation config ready: " + AnimationConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationConfig EnsureAnimationConfig()
|
||||||
|
{
|
||||||
|
var config = AssetDatabase.LoadAssetAtPath<AnimationConfig>(AnimationConfigPath);
|
||||||
|
if (config != null)
|
||||||
|
return config;
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(AnimationConfigPath);
|
||||||
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
config = ScriptableObject.CreateInstance<AnimationConfig>();
|
||||||
|
config.name = "AnimationConfig";
|
||||||
|
AssetDatabase.CreateAsset(config, AnimationConfigPath);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
private static PlaymatLayoutConfig EnsurePlaymatConfig()
|
private static PlaymatLayoutConfig EnsurePlaymatConfig()
|
||||||
{
|
{
|
||||||
var config = AssetDatabase.LoadAssetAtPath<PlaymatLayoutConfig>(PlaymatConfigPath);
|
var config = AssetDatabase.LoadAssetAtPath<PlaymatLayoutConfig>(PlaymatConfigPath);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 0636c0f7bf02c0548adae5e43ddb8f26, type: 3}
|
||||||
|
m_Name: AnimationConfig
|
||||||
|
m_EditorClassIdentifier: Vanguard.Runtime::Vanguard.Battle.AnimationConfig
|
||||||
|
cardMoveSpeed: 14
|
||||||
|
cardRotateSpeed: 12
|
||||||
|
cardMoveDuration: 0.45
|
||||||
|
cardFlipDuration: 0.3
|
||||||
|
callMoveDuration: 0.55
|
||||||
|
retireDuration: 0.8
|
||||||
|
attackDeclare: 1.2
|
||||||
|
guard: 1.9
|
||||||
|
driveCheck: 1.5
|
||||||
|
trigger: 1.4
|
||||||
|
battleResolved: 1.3
|
||||||
|
damage: 1.6
|
||||||
|
heal: 1.4
|
||||||
|
intercept: 1.9
|
||||||
|
perfectGuard: 2
|
||||||
|
inputLockTail: 0.15
|
||||||
|
arrowShowDuration: 0.25
|
||||||
|
arrowHideDuration: 0.2
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 233e60b202fccbf4d868f32ee2a54777
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Vanguard.Battle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Длительности боевых анимаций и блокировки ввода. Ассет лежит в
|
||||||
|
/// Resources/AnimationConfig; если его нет, используются значения по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(menuName = "Vanguard/Animation Config", fileName = "AnimationConfig")]
|
||||||
|
public sealed class AnimationConfig : ScriptableObject
|
||||||
|
{
|
||||||
|
[Header("Карты")]
|
||||||
|
public float cardMoveSpeed = 14f;
|
||||||
|
public float cardRotateSpeed = 12f;
|
||||||
|
public float cardMoveDuration = 0.45f;
|
||||||
|
public float cardFlipDuration = 0.3f;
|
||||||
|
public float callMoveDuration = 0.55f;
|
||||||
|
public float retireDuration = 0.8f;
|
||||||
|
|
||||||
|
[Header("Бой")]
|
||||||
|
public float attackDeclare = 1.2f;
|
||||||
|
public float guard = 1.9f;
|
||||||
|
public float driveCheck = 1.5f;
|
||||||
|
public float trigger = 1.4f;
|
||||||
|
public float battleResolved = 1.3f;
|
||||||
|
public float damage = 1.6f;
|
||||||
|
public float heal = 1.4f;
|
||||||
|
public float intercept = 1.9f;
|
||||||
|
public float perfectGuard = 2.0f;
|
||||||
|
|
||||||
|
[Header("Ввод")]
|
||||||
|
public float inputLockTail = 0.15f;
|
||||||
|
|
||||||
|
[Header("Стрелка атаки")]
|
||||||
|
public float arrowShowDuration = 0.25f;
|
||||||
|
public float arrowHideDuration = 0.2f;
|
||||||
|
|
||||||
|
private static AnimationConfig _default;
|
||||||
|
|
||||||
|
public static AnimationConfig Default
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_default == null)
|
||||||
|
{
|
||||||
|
_default = CreateInstance<AnimationConfig>();
|
||||||
|
_default.name = "AnimationConfig (runtime)";
|
||||||
|
_default.hideFlags = HideFlags.DontSave;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AnimationConfig LoadOrDefault()
|
||||||
|
{
|
||||||
|
var loaded = Resources.Load<AnimationConfig>("AnimationConfig");
|
||||||
|
return loaded != null ? loaded : Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
cardMoveSpeed = Mathf.Max(1f, cardMoveSpeed);
|
||||||
|
cardRotateSpeed = Mathf.Max(1f, cardRotateSpeed);
|
||||||
|
cardMoveDuration = Mathf.Max(0.05f, cardMoveDuration);
|
||||||
|
cardFlipDuration = Mathf.Max(0.05f, cardFlipDuration);
|
||||||
|
callMoveDuration = Mathf.Max(0.05f, callMoveDuration);
|
||||||
|
retireDuration = Mathf.Max(0.05f, retireDuration);
|
||||||
|
attackDeclare = Mathf.Max(0.05f, attackDeclare);
|
||||||
|
guard = Mathf.Max(0.05f, guard);
|
||||||
|
driveCheck = Mathf.Max(0.05f, driveCheck);
|
||||||
|
trigger = Mathf.Max(0.05f, trigger);
|
||||||
|
battleResolved = Mathf.Max(0.05f, battleResolved);
|
||||||
|
damage = Mathf.Max(0.05f, damage);
|
||||||
|
heal = Mathf.Max(0.05f, heal);
|
||||||
|
intercept = Mathf.Max(0.05f, intercept);
|
||||||
|
perfectGuard = Mathf.Max(0.05f, perfectGuard);
|
||||||
|
inputLockTail = Mathf.Max(0f, inputLockTail);
|
||||||
|
arrowShowDuration = Mathf.Max(0.05f, arrowShowDuration);
|
||||||
|
arrowHideDuration = Mathf.Max(0.05f, arrowHideDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0636c0f7bf02c0548adae5e43ddb8f26
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Vanguard.Battle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Счётчик причин блокировки: анимация, оверлей, контекстное меню.
|
||||||
|
/// Ввод возвращается только когда снята последняя причина.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InputLockService
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, int> _reasons =
|
||||||
|
new Dictionary<string, int>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public bool IsLocked => _reasons.Count > 0;
|
||||||
|
|
||||||
|
public event Action<bool> Changed;
|
||||||
|
|
||||||
|
public void Acquire(string reason)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(reason))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var wasLocked = _reasons.Count > 0;
|
||||||
|
_reasons.TryGetValue(reason, out var count);
|
||||||
|
_reasons[reason] = count + 1;
|
||||||
|
|
||||||
|
if (!wasLocked)
|
||||||
|
Changed?.Invoke(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Release(string reason)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(reason))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_reasons.TryGetValue(reason, out var count))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (count > 1)
|
||||||
|
{
|
||||||
|
_reasons[reason] = count - 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_reasons.Remove(reason);
|
||||||
|
if (_reasons.Count == 0)
|
||||||
|
Changed?.Invoke(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseAll()
|
||||||
|
{
|
||||||
|
if (_reasons.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_reasons.Clear();
|
||||||
|
Changed?.Invoke(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 74c6d538edc00d040ac6486bfa5513ce
|
||||||
|
|
@ -51,10 +51,11 @@ MonoBehaviour:
|
||||||
opponentHandScale: 0.8
|
opponentHandScale: 0.8
|
||||||
opponentHandRoll: 2.5
|
opponentHandRoll: 2.5
|
||||||
guardianRowGap: 0.55
|
guardianRowGap: 0.55
|
||||||
guardianSpacing: 0.44
|
guardianSpacing: 0.62
|
||||||
guardianMaxWidth: 3.6
|
guardianMaxWidth: 3.6
|
||||||
damageX: 3
|
damageX: 3
|
||||||
pileX: 2.95
|
pileX: 2.95
|
||||||
|
showZoneLabels: 0
|
||||||
cameraPosition: {x: 1.4, y: 5.8, z: -8}
|
cameraPosition: {x: 1.4, y: 5.8, z: -8}
|
||||||
cameraTarget: {x: 1.4, y: 0, z: 0}
|
cameraTarget: {x: 1.4, y: 0, z: 0}
|
||||||
cameraFov: 46
|
cameraFov: 46
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ namespace Vanguard.Board3D
|
||||||
|
|
||||||
[Header("Круг защиты")]
|
[Header("Круг защиты")]
|
||||||
public float guardianRowGap = 0.55f;
|
public float guardianRowGap = 0.55f;
|
||||||
public float guardianSpacing = 0.44f;
|
public float guardianSpacing = 0.62f;
|
||||||
public float guardianMaxWidth = 3.6f;
|
public float guardianMaxWidth = 3.6f;
|
||||||
|
|
||||||
[Header("Стопки")]
|
[Header("Стопки")]
|
||||||
|
|
@ -81,6 +81,10 @@ namespace Vanguard.Board3D
|
||||||
[Tooltip("Расстояние от центра стола до колоды и дропа.")]
|
[Tooltip("Расстояние от центра стола до колоды и дропа.")]
|
||||||
public float pileX = 2.95f;
|
public float pileX = 2.95f;
|
||||||
|
|
||||||
|
[Header("Отладка")]
|
||||||
|
[Tooltip("Показывать подписи зон на 3D-столе.")]
|
||||||
|
public bool showZoneLabels = false;
|
||||||
|
|
||||||
[Header("Камера")]
|
[Header("Камера")]
|
||||||
[Tooltip("Фиксированная камера над полем. Не двигается игроком.")]
|
[Tooltip("Фиксированная камера над полем. Не двигается игроком.")]
|
||||||
public Vector3 cameraPosition = new Vector3(1.4f, 5.8f, -8.0f);
|
public Vector3 cameraPosition = new Vector3(1.4f, 5.8f, -8.0f);
|
||||||
|
|
|
||||||
|
|
@ -92,9 +92,10 @@ namespace Vanguard.Board3D
|
||||||
{
|
{
|
||||||
var x = (localSeat == 0 ? 1f : -1f) * _config.pileX;
|
var x = (localSeat == 0 ? 1f : -1f) * _config.pileX;
|
||||||
var offset = index - (Mathf.Max(1, count) - 1) * 0.5f;
|
var offset = index - (Mathf.Max(1, count) - 1) * 0.5f;
|
||||||
|
var sideSign = localSeat == 0 ? 1f : -1f;
|
||||||
// «Над колодой» — дальше от камеры, то есть выше на экране.
|
// «Над колодой» — дальше от камеры, то есть выше на экране.
|
||||||
var z = (ZSign(localSeat) * _config.frontRowZ) + 1.0f;
|
var z = (ZSign(localSeat) * _config.frontRowZ) + 1.0f;
|
||||||
return new Vector3(x + offset * 0.42f, _config.cardLift, z);
|
return new Vector3(x + offset * 0.42f * sideSign, _config.cardLift, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Душа — визуальная стопка под кругом V, чуть ближе к игроку.</summary>
|
/// <summary>Душа — визуальная стопка под кругом V, чуть ближе к игроку.</summary>
|
||||||
|
|
@ -113,7 +114,11 @@ namespace Vanguard.Board3D
|
||||||
|
|
||||||
var offset = index - (count - 1) * 0.5f;
|
var offset = index - (count - 1) * 0.5f;
|
||||||
var z = ZSign(localSeat) * _config.guardianRowGap;
|
var z = ZSign(localSeat) * _config.guardianRowGap;
|
||||||
return new Vector3(offset * spacing, _config.GuardianCenterY, z);
|
var stackLift = index * 0.0015f;
|
||||||
|
return new Vector3(
|
||||||
|
offset * spacing,
|
||||||
|
_config.GuardianCenterY + stackLift,
|
||||||
|
z);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Веер руки перед локальным игроком.</summary>
|
/// <summary>Веер руки перед локальным игроком.</summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using Vanguard.Battle;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Стрелка атаки на столе: линия от атакующего к цели и наконечник.
|
||||||
|
/// Точки обновляются каждый кадр, поэтому стрелка едет за картами.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AttackArrowView
|
||||||
|
{
|
||||||
|
private const float ShaftWidth = 0.075f;
|
||||||
|
private const float HeadWidth = 0.17f;
|
||||||
|
private const float HeadLength = 0.38f;
|
||||||
|
|
||||||
|
private GameObject _root;
|
||||||
|
private LineRenderer _shaft;
|
||||||
|
private LineRenderer _head;
|
||||||
|
private Material _material;
|
||||||
|
private AnimationConfig _config;
|
||||||
|
private Vector3 _from;
|
||||||
|
private Vector3 _to;
|
||||||
|
private bool _visible;
|
||||||
|
private float _alpha;
|
||||||
|
private float _widthScale = 1f;
|
||||||
|
|
||||||
|
public bool IsVisible => _visible || _alpha > 0.001f;
|
||||||
|
|
||||||
|
public void Build(Transform parent, AnimationConfig config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_root = new GameObject("Attack Arrow");
|
||||||
|
_root.transform.SetParent(parent, false);
|
||||||
|
|
||||||
|
var shader = Shader.Find("Sprites/Default");
|
||||||
|
if (shader == null)
|
||||||
|
shader = Shader.Find("Unlit/Color");
|
||||||
|
|
||||||
|
_material = shader != null ? new Material(shader) : null;
|
||||||
|
_shaft = CreateLine("Shaft", ShaftWidth);
|
||||||
|
_head = CreateLine("Head", ShaftWidth * 0.7f);
|
||||||
|
_head.positionCount = 3;
|
||||||
|
_root.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetConfig(AnimationConfig config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Show(Vector3 from, Vector3 to)
|
||||||
|
{
|
||||||
|
SetPoints(from, to);
|
||||||
|
_visible = true;
|
||||||
|
|
||||||
|
if (_root != null)
|
||||||
|
_root.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Hide()
|
||||||
|
{
|
||||||
|
_visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HideImmediate()
|
||||||
|
{
|
||||||
|
_visible = false;
|
||||||
|
_alpha = 0f;
|
||||||
|
if (_root != null)
|
||||||
|
_root.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPoints(Vector3 from, Vector3 to)
|
||||||
|
{
|
||||||
|
_from = from;
|
||||||
|
_to = to;
|
||||||
|
UpdateGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Tick(float deltaTime)
|
||||||
|
{
|
||||||
|
if (_root == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var target = _visible ? 1f : 0f;
|
||||||
|
var duration = _visible
|
||||||
|
? (_config != null ? _config.arrowShowDuration : 0.25f)
|
||||||
|
: (_config != null ? _config.arrowHideDuration : 0.2f);
|
||||||
|
_alpha = Mathf.MoveTowards(
|
||||||
|
_alpha,
|
||||||
|
target,
|
||||||
|
deltaTime / Mathf.Max(0.01f, duration));
|
||||||
|
|
||||||
|
if (!_visible && _alpha <= 0.001f)
|
||||||
|
{
|
||||||
|
_root.SetActive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_root.SetActive(true);
|
||||||
|
_widthScale = 0.55f + (_alpha * 0.45f);
|
||||||
|
UpdateGeometry();
|
||||||
|
ApplyColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_root != null)
|
||||||
|
{
|
||||||
|
if (Application.isPlaying)
|
||||||
|
Object.Destroy(_root);
|
||||||
|
else
|
||||||
|
Object.DestroyImmediate(_root);
|
||||||
|
_root = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_material != null)
|
||||||
|
{
|
||||||
|
if (Application.isPlaying)
|
||||||
|
Object.Destroy(_material);
|
||||||
|
else
|
||||||
|
Object.DestroyImmediate(_material);
|
||||||
|
_material = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LineRenderer CreateLine(string name, float width)
|
||||||
|
{
|
||||||
|
var go = new GameObject(name);
|
||||||
|
go.transform.SetParent(_root.transform, false);
|
||||||
|
var line = go.AddComponent<LineRenderer>();
|
||||||
|
line.useWorldSpace = false;
|
||||||
|
line.positionCount = 2;
|
||||||
|
line.widthMultiplier = width;
|
||||||
|
line.numCapVertices = 4;
|
||||||
|
line.numCornerVertices = 4;
|
||||||
|
line.alignment = LineAlignment.View;
|
||||||
|
line.textureMode = LineTextureMode.Stretch;
|
||||||
|
line.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||||
|
line.receiveShadows = false;
|
||||||
|
if (_material != null)
|
||||||
|
line.material = _material;
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateGeometry()
|
||||||
|
{
|
||||||
|
if (_shaft == null || _head == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var direction = _to - _from;
|
||||||
|
if (direction.sqrMagnitude < 0.0001f)
|
||||||
|
direction = Vector3.forward;
|
||||||
|
direction.Normalize();
|
||||||
|
|
||||||
|
_shaft.SetPosition(0, _from);
|
||||||
|
_shaft.SetPosition(1, _to);
|
||||||
|
_shaft.widthMultiplier = ShaftWidth * _widthScale;
|
||||||
|
|
||||||
|
var tip = _to;
|
||||||
|
var back = _to - (direction * HeadLength);
|
||||||
|
var side = Vector3.Cross(Vector3.up, direction).normalized;
|
||||||
|
if (side.sqrMagnitude < 0.0001f)
|
||||||
|
side = Vector3.right;
|
||||||
|
var half = side * (HeadWidth * _widthScale);
|
||||||
|
_head.SetPosition(0, back + half);
|
||||||
|
_head.SetPosition(1, tip);
|
||||||
|
_head.SetPosition(2, back - half);
|
||||||
|
_head.widthMultiplier = ShaftWidth * 0.7f * _widthScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyColor()
|
||||||
|
{
|
||||||
|
var color = new Color(1f, 0.72f, 0.26f, _alpha * 0.92f);
|
||||||
|
foreach (var line in new[] { _shaft, _head })
|
||||||
|
{
|
||||||
|
if (line == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
line.startColor = color;
|
||||||
|
line.endColor = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f6b37f3c1f69dcb419ed41db1abe16fc
|
||||||
|
|
@ -29,6 +29,8 @@ namespace Vanguard.Board3D
|
||||||
private bool _dragging;
|
private bool _dragging;
|
||||||
private Vector3 _dragOriginPosition;
|
private Vector3 _dragOriginPosition;
|
||||||
private Quaternion _dragOriginRotation;
|
private Quaternion _dragOriginRotation;
|
||||||
|
private float _moveSpeed = 16f;
|
||||||
|
private float _rotateSpeed = 14f;
|
||||||
|
|
||||||
public BoardCardRef Ref { get; private set; }
|
public BoardCardRef Ref { get; private set; }
|
||||||
public bool IsFaceUp => _faceUp;
|
public bool IsFaceUp => _faceUp;
|
||||||
|
|
@ -37,6 +39,11 @@ namespace Vanguard.Board3D
|
||||||
public Vector3 TargetPosition => _targetPosition;
|
public Vector3 TargetPosition => _targetPosition;
|
||||||
public float TargetYaw => _targetRotation.eulerAngles.y;
|
public float TargetYaw => _targetRotation.eulerAngles.y;
|
||||||
public Quaternion TargetRotation => _targetRotation;
|
public Quaternion TargetRotation => _targetRotation;
|
||||||
|
public bool IsAnimating =>
|
||||||
|
!_dragging
|
||||||
|
&& (Vector3.SqrMagnitude(transform.position - _targetPosition) > 0.0000004f
|
||||||
|
|| Quaternion.Angle(transform.rotation, _targetRotation) > 0.1f
|
||||||
|
|| Vector3.SqrMagnitude(transform.localScale - _targetScale) > 0.0000004f);
|
||||||
public string StatsText => _stats != null && _stats.gameObject.activeSelf
|
public string StatsText => _stats != null && _stats.gameObject.activeSelf
|
||||||
? _stats.text
|
? _stats.text
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -53,6 +60,8 @@ namespace Vanguard.Board3D
|
||||||
private void Build(PlaymatLayoutConfig config)
|
private void Build(PlaymatLayoutConfig config)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_moveSpeed = config.smoothMoveSpeed;
|
||||||
|
_rotateSpeed = config.smoothRotateSpeed;
|
||||||
_block = new MaterialPropertyBlock();
|
_block = new MaterialPropertyBlock();
|
||||||
|
|
||||||
var width = _config.cardWidth;
|
var width = _config.cardWidth;
|
||||||
|
|
@ -138,8 +147,13 @@ namespace Vanguard.Board3D
|
||||||
_front.SetPropertyBlock(_block);
|
_front.SetPropertyBlock(_block);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetStats(string text, bool modified)
|
public void SetAnimationSpeeds(float moveSpeed, float rotateSpeed)
|
||||||
{
|
{
|
||||||
|
_moveSpeed = Mathf.Max(1f, moveSpeed);
|
||||||
|
_rotateSpeed = Mathf.Max(1f, rotateSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetStats(string text, bool modified) {
|
||||||
if (_stats == null)
|
if (_stats == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -297,8 +311,8 @@ namespace Vanguard.Board3D
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var move = 1f - Mathf.Exp(-_config.smoothMoveSpeed * deltaTime);
|
var move = 1f - Mathf.Exp(-_moveSpeed * deltaTime);
|
||||||
var rotate = 1f - Mathf.Exp(-_config.smoothRotateSpeed * deltaTime);
|
var rotate = 1f - Mathf.Exp(-_rotateSpeed * deltaTime);
|
||||||
transform.position = Vector3.Lerp(transform.position, _targetPosition, move);
|
transform.position = Vector3.Lerp(transform.position, _targetPosition, move);
|
||||||
transform.rotation = Quaternion.Slerp(transform.rotation, _targetRotation, rotate);
|
transform.rotation = Quaternion.Slerp(transform.rotation, _targetRotation, rotate);
|
||||||
transform.localScale = Vector3.Lerp(transform.localScale, _targetScale, move);
|
transform.localScale = Vector3.Lerp(transform.localScale, _targetScale, move);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
using UnityEngine.InputSystem;
|
using UnityEngine.InputSystem;
|
||||||
|
using Vanguard.Battle;
|
||||||
using Vanguard.Shared.Protocol;
|
using Vanguard.Shared.Protocol;
|
||||||
|
|
||||||
namespace Vanguard.Board3D
|
namespace Vanguard.Board3D
|
||||||
|
|
@ -44,9 +45,13 @@ namespace Vanguard.Board3D
|
||||||
private Transform _pilesRoot;
|
private Transform _pilesRoot;
|
||||||
private Transform _zonesRoot;
|
private Transform _zonesRoot;
|
||||||
private Camera _camera;
|
private Camera _camera;
|
||||||
|
private InputLockService _inputLock;
|
||||||
|
private AnimationConfig _animationConfig;
|
||||||
|
private float _boardLockReleaseAt = -1f;
|
||||||
|
|
||||||
private BoardZoneView _guardianZone;
|
private BoardZoneView _guardianZone;
|
||||||
private BoardZoneView _dropIndicator;
|
private BoardZoneView _dropIndicator;
|
||||||
|
private AttackArrowView _attackArrow;
|
||||||
private Pile3D _deckSelf;
|
private Pile3D _deckSelf;
|
||||||
private Pile3D _deckOpponent;
|
private Pile3D _deckOpponent;
|
||||||
private Pile3D _dropSelf;
|
private Pile3D _dropSelf;
|
||||||
|
|
@ -72,8 +77,53 @@ namespace Vanguard.Board3D
|
||||||
public PlaymatLayoutConfig Config => _config;
|
public PlaymatLayoutConfig Config => _config;
|
||||||
public PlaymatLayout Layout => _layout;
|
public PlaymatLayout Layout => _layout;
|
||||||
public int SelfPlayer => _selfPlayer;
|
public int SelfPlayer => _selfPlayer;
|
||||||
|
public bool AttackArrowVisible =>
|
||||||
|
_attackArrow != null && _attackArrow.IsVisible;
|
||||||
public BoardArtProvider ArtProvider { get; set; }
|
public BoardArtProvider ArtProvider { get; set; }
|
||||||
|
|
||||||
|
public bool TryGetCardScreenPosition(BoardCardRef reference, out Vector2 screen)
|
||||||
|
{
|
||||||
|
screen = default;
|
||||||
|
if (_camera == null)
|
||||||
|
_camera = Camera.main;
|
||||||
|
if (_camera == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach (var card in _cards.Values)
|
||||||
|
{
|
||||||
|
if (card == null || !card.Ref.Equals(reference))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var point = _camera.WorldToScreenPoint(card.transform.position);
|
||||||
|
if (point.z <= 0f)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
screen = new Vector2(point.x, point.y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConfigureRuntime(
|
||||||
|
AnimationConfig animationConfig,
|
||||||
|
InputLockService inputLock)
|
||||||
|
{
|
||||||
|
_animationConfig = animationConfig;
|
||||||
|
_inputLock = inputLock;
|
||||||
|
_attackArrow?.SetConfig(_animationConfig);
|
||||||
|
|
||||||
|
if (_animationConfig == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var card in _cards.Values)
|
||||||
|
{
|
||||||
|
card.SetAnimationSpeeds(
|
||||||
|
_animationConfig.cardMoveSpeed,
|
||||||
|
_animationConfig.cardRotateSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public event Action<BoardCardRef> CardClicked;
|
public event Action<BoardCardRef> CardClicked;
|
||||||
public event Action<BoardCardRef> CardRightClicked;
|
public event Action<BoardCardRef> CardRightClicked;
|
||||||
public event Action<BoardCardRef> SlotClicked;
|
public event Action<BoardCardRef> SlotClicked;
|
||||||
|
|
@ -117,6 +167,7 @@ namespace Vanguard.Board3D
|
||||||
BuildZones();
|
BuildZones();
|
||||||
BuildPiles();
|
BuildPiles();
|
||||||
BuildDropIndicator();
|
BuildDropIndicator();
|
||||||
|
BuildAttackArrow();
|
||||||
|
|
||||||
_cardsRoot = new GameObject("Cards").transform;
|
_cardsRoot = new GameObject("Cards").transform;
|
||||||
_cardsRoot.SetParent(transform, false);
|
_cardsRoot.SetParent(transform, false);
|
||||||
|
|
@ -149,6 +200,8 @@ namespace Vanguard.Board3D
|
||||||
CancelPointer();
|
CancelPointer();
|
||||||
_hasRendered = false;
|
_hasRendered = false;
|
||||||
_handBindings.Clear();
|
_handBindings.Clear();
|
||||||
|
_attackArrow?.HideImmediate();
|
||||||
|
ReleaseBoardLock();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -232,15 +285,115 @@ namespace Vanguard.Board3D
|
||||||
|
|
||||||
UpdatePiles(view);
|
UpdatePiles(view);
|
||||||
UpdateSlots(state);
|
UpdateSlots(state);
|
||||||
|
UpdateAttackArrowState(view);
|
||||||
_hasRendered = true;
|
_hasRendered = true;
|
||||||
}
|
|
||||||
|
|
||||||
|
if (_inputLock != null
|
||||||
|
&& _animationConfig != null
|
||||||
|
&& AnyCardAnimating())
|
||||||
|
{
|
||||||
|
_inputLock.Acquire("board");
|
||||||
|
_boardLockReleaseAt = Time.unscaledTime
|
||||||
|
+ Mathf.Max(1f, _animationConfig.cardMoveDuration * 3f)
|
||||||
|
+ _animationConfig.inputLockTail;
|
||||||
|
}
|
||||||
|
}
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
ClearCards();
|
ClearCards();
|
||||||
_handBindings.Clear();
|
_handBindings.Clear();
|
||||||
_view = null;
|
_view = null;
|
||||||
_hasRendered = false;
|
_hasRendered = false;
|
||||||
|
_attackArrow?.HideImmediate();
|
||||||
|
ReleaseBoardLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAttackArrowState(MatchView view)
|
||||||
|
{
|
||||||
|
if (_attackArrow == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (view?.Battle == null
|
||||||
|
|| !TryResolveAttackPoints(view, out var from, out var to))
|
||||||
|
{
|
||||||
|
_attackArrow.Hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_attackArrow.Show(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAttackArrowPoints()
|
||||||
|
{
|
||||||
|
if (_attackArrow == null || !_attackArrow.IsVisible || _view?.Battle == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (TryResolveAttackPoints(_view, out var from, out var to))
|
||||||
|
_attackArrow.SetPoints(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolveAttackPoints(
|
||||||
|
MatchView view,
|
||||||
|
out Vector3 from,
|
||||||
|
out Vector3 to)
|
||||||
|
{
|
||||||
|
from = default;
|
||||||
|
to = default;
|
||||||
|
var battle = view?.Battle;
|
||||||
|
if (battle == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var attacker = BattleCard(view, battle.AttackerPlayer, battle.AttackerSlot);
|
||||||
|
var defenderIndex = 1 - battle.AttackerPlayer;
|
||||||
|
var defender = BattleCard(view, defenderIndex, battle.TargetSlot);
|
||||||
|
if (attacker == null || defender == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
from = ArrowAnchor(
|
||||||
|
BoardCardRef.Field(battle.AttackerPlayer, battle.AttackerSlot, attacker),
|
||||||
|
battle.AttackerPlayer,
|
||||||
|
battle.AttackerSlot);
|
||||||
|
to = ArrowAnchor(
|
||||||
|
BoardCardRef.Field(defenderIndex, battle.TargetSlot, defender),
|
||||||
|
defenderIndex,
|
||||||
|
battle.TargetSlot);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3 ArrowAnchor(BoardCardRef reference, int player, int slot)
|
||||||
|
{
|
||||||
|
var card = FindCard(reference);
|
||||||
|
if (card != null)
|
||||||
|
return card.transform.localPosition + (Vector3.up * 0.2f);
|
||||||
|
|
||||||
|
return _layout.CirclePositionForPlayer(player, _selfPlayer, slot)
|
||||||
|
+ (Vector3.up * 0.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CardView BattleCard(MatchView view, int playerIndex, int slot)
|
||||||
|
{
|
||||||
|
var player = view?.Players.FirstOrDefault(item => item.Index == playerIndex);
|
||||||
|
if (player == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (slot < 0)
|
||||||
|
return player.Vanguard;
|
||||||
|
|
||||||
|
return player.RearGuards != null
|
||||||
|
&& slot < player.RearGuards.Length
|
||||||
|
? player.RearGuards[slot]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Card3D FindCard(BoardCardRef reference)
|
||||||
|
{
|
||||||
|
foreach (var card in _cards.Values)
|
||||||
|
{
|
||||||
|
if (card != null && card.Ref.Equals(reference))
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildTable()
|
private void BuildTable()
|
||||||
|
|
@ -376,7 +529,7 @@ namespace Vanguard.Board3D
|
||||||
1.05f,
|
1.05f,
|
||||||
Mathf.Max(0.016f, _config.cardWidth * 0.025f),
|
Mathf.Max(0.016f, _config.cardWidth * 0.025f),
|
||||||
new Color(0.30f, 0.18f, 0.52f),
|
new Color(0.30f, 0.18f, 0.52f),
|
||||||
"TRIGGER",
|
_config.showZoneLabels ? "TRIGGER" : string.Empty,
|
||||||
0.04f,
|
0.04f,
|
||||||
zSign * 0.72f,
|
zSign * 0.72f,
|
||||||
new Color(0.82f, 0.76f, 1f, 0.75f),
|
new Color(0.82f, 0.76f, 1f, 0.75f),
|
||||||
|
|
@ -389,10 +542,22 @@ namespace Vanguard.Board3D
|
||||||
_pilesRoot = new GameObject("Piles").transform;
|
_pilesRoot = new GameObject("Piles").transform;
|
||||||
_pilesRoot.SetParent(transform, false);
|
_pilesRoot.SetParent(transform, false);
|
||||||
|
|
||||||
_deckSelf = CreatePile(BoardZone.Deck, 0, 0, "DECK", new Color(0.20f, 0.34f, 0.60f));
|
_deckSelf = CreatePile(
|
||||||
_deckOpponent = CreatePile(BoardZone.Deck, 1, 1, "DECK", new Color(0.60f, 0.26f, 0.26f));
|
BoardZone.Deck, 0, 0,
|
||||||
_dropSelf = CreatePile(BoardZone.Drop, 0, 0, "DROP", new Color(0.30f, 0.30f, 0.40f));
|
_config.showZoneLabels ? "DECK" : string.Empty,
|
||||||
_dropOpponent = CreatePile(BoardZone.Drop, 1, 1, "DROP", new Color(0.40f, 0.28f, 0.28f));
|
new Color(0.20f, 0.34f, 0.60f));
|
||||||
|
_deckOpponent = CreatePile(
|
||||||
|
BoardZone.Deck, 1, 1,
|
||||||
|
_config.showZoneLabels ? "DECK" : string.Empty,
|
||||||
|
new Color(0.60f, 0.26f, 0.26f));
|
||||||
|
_dropSelf = CreatePile(
|
||||||
|
BoardZone.Drop, 0, 0,
|
||||||
|
_config.showZoneLabels ? "DROP" : string.Empty,
|
||||||
|
new Color(0.30f, 0.30f, 0.40f));
|
||||||
|
_dropOpponent = CreatePile(
|
||||||
|
BoardZone.Drop, 1, 1,
|
||||||
|
_config.showZoneLabels ? "DROP" : string.Empty,
|
||||||
|
new Color(0.40f, 0.28f, 0.28f));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pile3D CreatePile(
|
private Pile3D CreatePile(
|
||||||
|
|
@ -439,6 +604,12 @@ namespace Vanguard.Board3D
|
||||||
_dropIndicator.gameObject.SetActive(false);
|
_dropIndicator.gameObject.SetActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildAttackArrow()
|
||||||
|
{
|
||||||
|
_attackArrow = new AttackArrowView();
|
||||||
|
_attackArrow.Build(transform, _animationConfig);
|
||||||
|
}
|
||||||
|
|
||||||
private void EnsureCamera()
|
private void EnsureCamera()
|
||||||
{
|
{
|
||||||
if (_camera == null)
|
if (_camera == null)
|
||||||
|
|
@ -540,7 +711,10 @@ namespace Vanguard.Board3D
|
||||||
Ref = BoardCardRef.Guardian(player.Index, i, card),
|
Ref = BoardCardRef.Guardian(player.Index, i, card),
|
||||||
Art = ResolveArt(card.PrintingId),
|
Art = ResolveArt(card.PrintingId),
|
||||||
Position = _layout.GuardianPosition(localSeat, i, total),
|
Position = _layout.GuardianPosition(localSeat, i, total),
|
||||||
BaseRotation = Quaternion.Euler(0f, _layout.BaseYaw(localSeat), 0f),
|
BaseRotation = Quaternion.Euler(
|
||||||
|
0f,
|
||||||
|
_layout.BaseYaw(localSeat) + 90f,
|
||||||
|
0f),
|
||||||
FaceUp = true,
|
FaceUp = true,
|
||||||
Rested = false,
|
Rested = false,
|
||||||
});
|
});
|
||||||
|
|
@ -558,7 +732,10 @@ namespace Vanguard.Board3D
|
||||||
localSeat,
|
localSeat,
|
||||||
guardians.Count + i,
|
guardians.Count + i,
|
||||||
total),
|
total),
|
||||||
BaseRotation = Quaternion.Euler(0f, _layout.BaseYaw(localSeat), 0f),
|
BaseRotation = Quaternion.Euler(
|
||||||
|
0f,
|
||||||
|
_layout.BaseYaw(localSeat) + 90f,
|
||||||
|
0f),
|
||||||
FaceUp = true,
|
FaceUp = true,
|
||||||
Rested = false,
|
Rested = false,
|
||||||
});
|
});
|
||||||
|
|
@ -617,8 +794,10 @@ namespace Vanguard.Board3D
|
||||||
Ref = BoardCardRef.Trigger(player.Index, i, card),
|
Ref = BoardCardRef.Trigger(player.Index, i, card),
|
||||||
Art = ResolveArt(card.PrintingId),
|
Art = ResolveArt(card.PrintingId),
|
||||||
Position = _layout.TriggerPosition(localSeat, i, count),
|
Position = _layout.TriggerPosition(localSeat, i, count),
|
||||||
// Карты проверки всегда развёрнуты к локальному игроку.
|
BaseRotation = Quaternion.Euler(
|
||||||
BaseRotation = Quaternion.identity,
|
0f,
|
||||||
|
_layout.BaseYaw(localSeat),
|
||||||
|
0f),
|
||||||
FaceUp = true,
|
FaceUp = true,
|
||||||
Rested = false,
|
Rested = false,
|
||||||
});
|
});
|
||||||
|
|
@ -811,15 +990,15 @@ namespace Vanguard.Board3D
|
||||||
|
|
||||||
if (player == null)
|
if (player == null)
|
||||||
{
|
{
|
||||||
deck.SetCount(0, null, "DECK", Color.white);
|
deck.SetCount(0, null, _config.showZoneLabels ? "DECK" : string.Empty, Color.white);
|
||||||
drop.SetCount(0, null, "DROP", Color.white);
|
drop.SetCount(0, null, _config.showZoneLabels ? "DROP" : string.Empty, Color.white);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
deck.SetCount(
|
deck.SetCount(
|
||||||
player.DeckCount,
|
player.DeckCount,
|
||||||
null,
|
null,
|
||||||
"DECK",
|
_config.showZoneLabels ? "DECK" : string.Empty,
|
||||||
new Color(0.20f, 0.34f, 0.60f));
|
new Color(0.20f, 0.34f, 0.60f));
|
||||||
|
|
||||||
var topDrop = player.Drop != null && player.Drop.Count > 0
|
var topDrop = player.Drop != null && player.Drop.Count > 0
|
||||||
|
|
@ -828,7 +1007,7 @@ namespace Vanguard.Board3D
|
||||||
drop.SetCount(
|
drop.SetCount(
|
||||||
player.Drop?.Count ?? 0,
|
player.Drop?.Count ?? 0,
|
||||||
topDrop != null ? ResolveArt(topDrop.PrintingId).Front : null,
|
topDrop != null ? ResolveArt(topDrop.PrintingId).Front : null,
|
||||||
"DROP",
|
_config.showZoneLabels ? "DROP" : string.Empty,
|
||||||
new Color(0.30f, 0.30f, 0.40f));
|
new Color(0.30f, 0.30f, 0.40f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -863,6 +1042,14 @@ namespace Vanguard.Board3D
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
|
if (_boardLockReleaseAt > 0f
|
||||||
|
&& (!AnyCardAnimating()
|
||||||
|
|| Time.unscaledTime >= _boardLockReleaseAt))
|
||||||
|
{
|
||||||
|
_boardLockReleaseAt = -1f;
|
||||||
|
_inputLock?.Release("board");
|
||||||
|
}
|
||||||
|
|
||||||
UpdateHandView();
|
UpdateHandView();
|
||||||
|
|
||||||
var delta = Time.unscaledDeltaTime;
|
var delta = Time.unscaledDeltaTime;
|
||||||
|
|
@ -872,6 +1059,16 @@ namespace Vanguard.Board3D
|
||||||
card.Tick(delta);
|
card.Tick(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_attackArrow?.Tick(delta);
|
||||||
|
UpdateAttackArrowPoints();
|
||||||
|
|
||||||
|
if (_inputLock != null && _inputLock.IsLocked)
|
||||||
|
{
|
||||||
|
if (_mode != PointerMode.None || _dragging)
|
||||||
|
CancelPointer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
HandleRightClick();
|
HandleRightClick();
|
||||||
HandlePointer();
|
HandlePointer();
|
||||||
}
|
}
|
||||||
|
|
@ -1185,13 +1382,13 @@ namespace Vanguard.Board3D
|
||||||
if (pooled == null)
|
if (pooled == null)
|
||||||
continue;
|
continue;
|
||||||
pooled.gameObject.SetActive(true);
|
pooled.gameObject.SetActive(true);
|
||||||
return pooled;
|
return ConfigureCardSpeeds(pooled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_config.cardPrefab != null)
|
if (_config.cardPrefab != null)
|
||||||
{
|
{
|
||||||
var instance = Instantiate(_config.cardPrefab, _cardsRoot);
|
var instance = Instantiate(_config.cardPrefab, _cardsRoot);
|
||||||
return instance;
|
return ConfigureCardSpeeds(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_cardsRoot == null)
|
if (_cardsRoot == null)
|
||||||
|
|
@ -1200,7 +1397,19 @@ namespace Vanguard.Board3D
|
||||||
_cardsRoot.SetParent(transform, false);
|
_cardsRoot.SetParent(transform, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Card3D.Create(_cardsRoot, _config);
|
return ConfigureCardSpeeds(Card3D.Create(_cardsRoot, _config));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Card3D ConfigureCardSpeeds(Card3D card)
|
||||||
|
{
|
||||||
|
if (card != null && _animationConfig != null)
|
||||||
|
{
|
||||||
|
card.SetAnimationSpeeds(
|
||||||
|
_animationConfig.cardMoveSpeed,
|
||||||
|
_animationConfig.cardRotateSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReleaseCard(Card3D card)
|
private void ReleaseCard(Card3D card)
|
||||||
|
|
@ -1289,6 +1498,29 @@ namespace Vanguard.Board3D
|
||||||
{
|
{
|
||||||
if (_instance == this)
|
if (_instance == this)
|
||||||
_instance = null;
|
_instance = null;
|
||||||
|
|
||||||
|
_attackArrow?.Dispose();
|
||||||
|
ReleaseBoardLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseBoardLock()
|
||||||
|
{
|
||||||
|
if (_boardLockReleaseAt <= 0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_boardLockReleaseAt = -1f;
|
||||||
|
_inputLock?.Release("board");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool AnyCardAnimating()
|
||||||
|
{
|
||||||
|
foreach (var card in _cards.Values)
|
||||||
|
{
|
||||||
|
if (card != null && card.IsAnimating)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class HandCardBinding
|
private sealed class HandCardBinding
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private readonly MetaService _meta;
|
private readonly MetaService _meta;
|
||||||
private readonly Action _onExit;
|
private readonly Action _onExit;
|
||||||
|
private readonly AnimationConfig _animationConfig;
|
||||||
|
private readonly InputLockService _inputLock;
|
||||||
|
private bool _feedbackLockHeld;
|
||||||
private readonly Dictionary<string, CardCatalogItemDto> _catalog =
|
private readonly Dictionary<string, CardCatalogItemDto> _catalog =
|
||||||
new Dictionary<string, CardCatalogItemDto>(StringComparer.Ordinal);
|
new Dictionary<string, CardCatalogItemDto>(StringComparer.Ordinal);
|
||||||
private bool _catalogLoading;
|
private bool _catalogLoading;
|
||||||
|
|
@ -79,7 +82,10 @@ namespace Vanguard.UI
|
||||||
private OverlayNavigationService _overlays;
|
private OverlayNavigationService _overlays;
|
||||||
private ZoneBlockStripView _selfZones;
|
private ZoneBlockStripView _selfZones;
|
||||||
private ZoneBlockStripView _opponentZones;
|
private ZoneBlockStripView _opponentZones;
|
||||||
|
private CardActionMenuView _cardMenu;
|
||||||
private RectTransform _phasePanel;
|
private RectTransform _phasePanel;
|
||||||
|
private RectTransform _combatStats;
|
||||||
|
private Text _combatStatsText;
|
||||||
private bool _autoFinishMulligan;
|
private bool _autoFinishMulligan;
|
||||||
private float _autoFinishMulliganAt;
|
private float _autoFinishMulliganAt;
|
||||||
private bool _autoEndBattleArmed;
|
private bool _autoEndBattleArmed;
|
||||||
|
|
@ -89,6 +95,9 @@ namespace Vanguard.UI
|
||||||
{
|
{
|
||||||
_meta = meta;
|
_meta = meta;
|
||||||
_onExit = onExit;
|
_onExit = onExit;
|
||||||
|
_animationConfig = AnimationConfig.LoadOrDefault();
|
||||||
|
_inputLock = new InputLockService();
|
||||||
|
_inputLock.Changed += OnInputLockChanged;
|
||||||
|
|
||||||
_root = UiKit.CreatePanel(parent, "BattleScreen", Background);
|
_root = UiKit.CreatePanel(parent, "BattleScreen", Background);
|
||||||
var rootImage = _root.GetComponent<Image>();
|
var rootImage = _root.GetComponent<Image>();
|
||||||
|
|
@ -187,8 +196,12 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
BuildOverlaysAndZones();
|
BuildOverlaysAndZones();
|
||||||
BuildPhasePanel();
|
BuildPhasePanel();
|
||||||
|
BuildCombatStats();
|
||||||
|
_cardMenu = new CardActionMenuView();
|
||||||
|
_cardMenu.Build(_root);
|
||||||
|
|
||||||
_board3D = GameBoard3D.Create(null, PlaymatLayoutConfig.Default);
|
_board3D = GameBoard3D.Create(null, PlaymatLayoutConfig.Default);
|
||||||
|
_board3D.ConfigureRuntime(_animationConfig, _inputLock);
|
||||||
_board3D.ArtProvider = ResolveBoardArt;
|
_board3D.ArtProvider = ResolveBoardArt;
|
||||||
_board3D.CardClicked += OnBoard3DCardClicked;
|
_board3D.CardClicked += OnBoard3DCardClicked;
|
||||||
_board3D.CardRightClicked += OnBoard3DCardRightClicked;
|
_board3D.CardRightClicked += OnBoard3DCardRightClicked;
|
||||||
|
|
@ -223,6 +236,8 @@ namespace Vanguard.UI
|
||||||
_autoFinishMulliganAt = 0f;
|
_autoFinishMulliganAt = 0f;
|
||||||
_autoEndBattleArmed = false;
|
_autoEndBattleArmed = false;
|
||||||
_autoEndBattleAt = 0f;
|
_autoEndBattleAt = 0f;
|
||||||
|
_inputLock.ReleaseAll();
|
||||||
|
_feedbackLockHeld = false;
|
||||||
_logLines.Clear();
|
_logLines.Clear();
|
||||||
RebuildLog();
|
RebuildLog();
|
||||||
_feedbackQueue.Clear();
|
_feedbackQueue.Clear();
|
||||||
|
|
@ -231,6 +246,7 @@ namespace Vanguard.UI
|
||||||
_feedback.gameObject.SetActive(false);
|
_feedback.gameObject.SetActive(false);
|
||||||
_flash.gameObject.SetActive(false);
|
_flash.gameObject.SetActive(false);
|
||||||
_skipFeedback.gameObject.SetActive(false);
|
_skipFeedback.gameObject.SetActive(false);
|
||||||
|
_combatStats?.gameObject.SetActive(false);
|
||||||
_board3D?.Clear();
|
_board3D?.Clear();
|
||||||
_board3D?.SetVisible(true);
|
_board3D?.SetVisible(true);
|
||||||
_overlays?.CloseAll();
|
_overlays?.CloseAll();
|
||||||
|
|
@ -256,14 +272,18 @@ namespace Vanguard.UI
|
||||||
_ = connection.DisposeAsync().AsTask();
|
_ = connection.DisposeAsync().AsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_view = null;
|
||||||
_root.gameObject.SetActive(false);
|
_root.gameObject.SetActive(false);
|
||||||
_result.gameObject.SetActive(false);
|
_result.gameObject.SetActive(false);
|
||||||
_feedback.gameObject.SetActive(false);
|
_feedback.gameObject.SetActive(false);
|
||||||
_flash.gameObject.SetActive(false);
|
_flash.gameObject.SetActive(false);
|
||||||
_skipFeedback.gameObject.SetActive(false);
|
_skipFeedback.gameObject.SetActive(false);
|
||||||
_overlays?.CloseAll();
|
_overlays?.CloseAll();
|
||||||
|
_cardMenu?.Hide();
|
||||||
|
_combatStats?.gameObject.SetActive(false);
|
||||||
_board3D?.SetVisible(false);
|
_board3D?.SetVisible(false);
|
||||||
_view = null;
|
ReleaseFeedbackLock();
|
||||||
|
_inputLock.ReleaseAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tick()
|
public void Tick()
|
||||||
|
|
@ -281,7 +301,7 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private void UpdateAutoPhases()
|
private void UpdateAutoPhases()
|
||||||
{
|
{
|
||||||
if (_view == null || _sending)
|
if (_view == null || _sending || _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
|
@ -356,9 +376,16 @@ namespace Vanguard.UI
|
||||||
{
|
{
|
||||||
_feedbackPlaying = false;
|
_feedbackPlaying = false;
|
||||||
_skipFeedback.gameObject.SetActive(false);
|
_skipFeedback.gameObject.SetActive(false);
|
||||||
|
ReleaseFeedbackLock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_feedbackLockHeld)
|
||||||
|
{
|
||||||
|
_feedbackLockHeld = true;
|
||||||
|
_inputLock.Acquire("feedback");
|
||||||
|
}
|
||||||
|
|
||||||
var step = _feedbackQueue.Dequeue();
|
var step = _feedbackQueue.Dequeue();
|
||||||
_feedbackPlaying = true;
|
_feedbackPlaying = true;
|
||||||
_feedback.text = step.Text;
|
_feedback.text = step.Text;
|
||||||
|
|
@ -376,6 +403,32 @@ namespace Vanguard.UI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReleaseFeedbackLock()
|
||||||
|
{
|
||||||
|
if (!_feedbackLockHeld)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_feedbackLockHeld = false;
|
||||||
|
_inputLock.Release("feedback");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInputLockChanged(bool locked)
|
||||||
|
{
|
||||||
|
if (_view == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RebuildIntents(_view);
|
||||||
|
RebuildPhaseControls(_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnOverlayOpenChanged(bool open)
|
||||||
|
{
|
||||||
|
if (open)
|
||||||
|
_inputLock.Acquire("overlay");
|
||||||
|
else
|
||||||
|
_inputLock.Release("overlay");
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateFeedback()
|
private void UpdateFeedback()
|
||||||
{
|
{
|
||||||
if (!_feedbackPlaying)
|
if (!_feedbackPlaying)
|
||||||
|
|
@ -407,6 +460,7 @@ namespace Vanguard.UI
|
||||||
_feedback.gameObject.SetActive(false);
|
_feedback.gameObject.SetActive(false);
|
||||||
_flash.gameObject.SetActive(false);
|
_flash.gameObject.SetActive(false);
|
||||||
_skipFeedback.gameObject.SetActive(false);
|
_skipFeedback.gameObject.SetActive(false);
|
||||||
|
ReleaseFeedbackLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly struct FeedbackStep
|
private readonly struct FeedbackStep
|
||||||
|
|
@ -598,46 +652,46 @@ namespace Vanguard.UI
|
||||||
case AttackDeclaredView attack:
|
case AttackDeclaredView attack:
|
||||||
ShowFeedback("АТАКА " + SlotName(attack.AttackerSlot)
|
ShowFeedback("АТАКА " + SlotName(attack.AttackerSlot)
|
||||||
+ " → " + SlotName(attack.TargetSlot),
|
+ " → " + SlotName(attack.TargetSlot),
|
||||||
new Color(1f, 0.82f, 0.35f), 1.2f);
|
new Color(1f, 0.82f, 0.35f), _animationConfig.attackDeclare);
|
||||||
break;
|
break;
|
||||||
case GuardAddedView guard:
|
case GuardAddedView guard:
|
||||||
ShowFeedback("GUARD " + CardName(guard.PrintingId)
|
ShowFeedback("GUARD " + CardName(guard.PrintingId)
|
||||||
+ " +" + guard.Shield,
|
+ " +" + guard.Shield,
|
||||||
new Color(0.45f, 0.82f, 1f), 1.9f);
|
new Color(0.45f, 0.82f, 1f), _animationConfig.guard);
|
||||||
break;
|
break;
|
||||||
case DriveCheckedView drive:
|
case DriveCheckedView drive:
|
||||||
ShowFeedback("DRIVE " + CardName(drive.PrintingId)
|
ShowFeedback("DRIVE " + CardName(drive.PrintingId)
|
||||||
+ " · " + TriggerShort(drive.Trigger.ToString()),
|
+ " · " + TriggerShort(drive.Trigger.ToString()),
|
||||||
new Color(0.72f, 0.88f, 1f), 1.5f);
|
new Color(0.72f, 0.88f, 1f), _animationConfig.driveCheck);
|
||||||
break;
|
break;
|
||||||
case TriggerResolvedView trigger:
|
case TriggerResolvedView trigger:
|
||||||
ShowFeedback("TRIGGER " + CardName(trigger.PrintingId),
|
ShowFeedback("TRIGGER " + CardName(trigger.PrintingId),
|
||||||
new Color(0.72f, 0.88f, 1f), 1.4f);
|
new Color(0.72f, 0.88f, 1f), _animationConfig.trigger);
|
||||||
break;
|
break;
|
||||||
case BattleResolvedView battle:
|
case BattleResolvedView battle:
|
||||||
ShowFeedback(battle.Hit ? "HIT" : "BLOCK",
|
ShowFeedback(battle.Hit ? "HIT" : "BLOCK",
|
||||||
battle.Hit ? new Color(1f, 0.45f, 0.35f) : new Color(0.45f, 0.82f, 1f),
|
battle.Hit ? new Color(1f, 0.45f, 0.35f) : new Color(0.45f, 0.82f, 1f),
|
||||||
1.3f);
|
_animationConfig.battleResolved);
|
||||||
break;
|
break;
|
||||||
case DamageTakenView damage:
|
case DamageTakenView damage:
|
||||||
ShowFeedback("+" + damage.Amount + " DAMAGE",
|
ShowFeedback("+" + damage.Amount + " DAMAGE",
|
||||||
new Color(1f, 0.30f, 0.26f), 1.6f, flash: true);
|
new Color(1f, 0.30f, 0.26f), _animationConfig.damage, flash: true);
|
||||||
break;
|
break;
|
||||||
case HealedView:
|
case HealedView:
|
||||||
ShowFeedback("HEAL",
|
ShowFeedback("HEAL",
|
||||||
new Color(0.45f, 0.95f, 0.62f), 1.4f, flash: true);
|
new Color(0.45f, 0.95f, 0.62f), _animationConfig.heal, flash: true);
|
||||||
break;
|
break;
|
||||||
case UnitRetiredView:
|
case UnitRetiredView:
|
||||||
ShowFeedback("RETIRE",
|
ShowFeedback("RETIRE",
|
||||||
new Color(1f, 0.55f, 0.30f), 1.2f);
|
new Color(1f, 0.55f, 0.30f), _animationConfig.retireDuration);
|
||||||
break;
|
break;
|
||||||
case InterceptedView intercept:
|
case InterceptedView intercept:
|
||||||
ShowFeedback("INTERCEPT " + CardName(intercept.PrintingId),
|
ShowFeedback("INTERCEPT " + CardName(intercept.PrintingId),
|
||||||
new Color(0.55f, 0.82f, 1f), 1.9f);
|
new Color(0.55f, 0.82f, 1f), _animationConfig.intercept);
|
||||||
break;
|
break;
|
||||||
case PerfectGuardView perfect:
|
case PerfectGuardView perfect:
|
||||||
ShowFeedback("PERFECT GUARD " + CardName(perfect.SentinelPrintingId),
|
ShowFeedback("PERFECT GUARD " + CardName(perfect.SentinelPrintingId),
|
||||||
new Color(0.60f, 0.82f, 1f), 2.0f);
|
new Color(0.60f, 0.82f, 1f), _animationConfig.perfectGuard);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -734,6 +788,9 @@ namespace Vanguard.UI
|
||||||
if (_sending || _connection == null || _cts == null || _view == null)
|
if (_sending || _connection == null || _cts == null || _view == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (_inputLock.IsLocked && intent is not ConcedeIntent)
|
||||||
|
return;
|
||||||
|
|
||||||
if (_abilityDamageIndex >= 0)
|
if (_abilityDamageIndex >= 0)
|
||||||
{
|
{
|
||||||
if (intent is ActivateAbilityIntent activate)
|
if (intent is ActivateAbilityIntent activate)
|
||||||
|
|
@ -850,8 +907,10 @@ namespace Vanguard.UI
|
||||||
if (_view == null)
|
if (_view == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
_cardMenu?.Hide();
|
||||||
_board3D?.SetSelfPlayer(_view.You);
|
_board3D?.SetSelfPlayer(_view.You);
|
||||||
_board3D?.Render(_view, BuildBoardRenderState());
|
_board3D?.Render(_view, BuildBoardRenderState());
|
||||||
|
UpdateCombatStats();
|
||||||
UpdateZoneStrips();
|
UpdateZoneStrips();
|
||||||
|
|
||||||
RebuildIntents(_view);
|
RebuildIntents(_view);
|
||||||
|
|
@ -965,9 +1024,18 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private void OnBoard3DCardClicked(BoardCardRef card)
|
private void OnBoard3DCardClicked(BoardCardRef card)
|
||||||
{
|
{
|
||||||
if (_view == null || _sending)
|
if (_view == null || _sending || _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var targetSelection = _pendingIntents.Count > 0
|
||||||
|
|| IsMulliganSelecting()
|
||||||
|
|| IsDamageChoice()
|
||||||
|
|| IsAbilityDamageTargeting()
|
||||||
|
|| _view.Trigger != null
|
||||||
|
|| _view.Choice != null
|
||||||
|
|| _view.Ability != null
|
||||||
|
|| _view.Order != null;
|
||||||
|
|
||||||
switch (card.Zone)
|
switch (card.Zone)
|
||||||
{
|
{
|
||||||
case BoardZone.Hand:
|
case BoardZone.Hand:
|
||||||
|
|
@ -982,13 +1050,24 @@ namespace Vanguard.UI
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BoardZone.Vanguard:
|
case BoardZone.Vanguard:
|
||||||
OnBoardTargetClicked(new BoardTarget(
|
|
||||||
card.Player, -1, card.PrintingId, card.InstanceId, false));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BoardZone.RearGuard:
|
case BoardZone.RearGuard:
|
||||||
OnBoardTargetClicked(new BoardTarget(
|
if (targetSelection)
|
||||||
card.Player, card.Slot, card.PrintingId, card.InstanceId, false));
|
{
|
||||||
|
OnBoardTargetClicked(new BoardTarget(
|
||||||
|
card.Player,
|
||||||
|
card.Zone == BoardZone.Vanguard ? -1 : card.Slot,
|
||||||
|
card.PrintingId,
|
||||||
|
card.InstanceId,
|
||||||
|
false));
|
||||||
|
}
|
||||||
|
else if (card.Player == _view.You)
|
||||||
|
{
|
||||||
|
OpenCardMenu(card);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OpenCardDetails(card);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BoardZone.Damage:
|
case BoardZone.Damage:
|
||||||
|
|
@ -1000,14 +1079,144 @@ namespace Vanguard.UI
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BoardZone.Guardian:
|
case BoardZone.Guardian:
|
||||||
SetStatus("Карта в круге защиты.", Muted);
|
case BoardZone.Trigger:
|
||||||
|
OpenCardDetails(card);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OpenCardMenu(BoardCardRef card)
|
||||||
|
{
|
||||||
|
if (_view == null || _board3D == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var target = new BoardTarget(
|
||||||
|
card.Player,
|
||||||
|
card.Zone == BoardZone.Vanguard ? -1 : card.Slot,
|
||||||
|
card.PrintingId,
|
||||||
|
card.InstanceId,
|
||||||
|
false);
|
||||||
|
|
||||||
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
var sources = legal
|
||||||
|
.Where(intent => TryGetSource(intent, out var source) && source.Matches(target))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var actions = new List<CardActionMenuItem>();
|
||||||
|
var attacks = sources.OfType<AttackIntent>().ToList();
|
||||||
|
foreach (var group in attacks.GroupBy(attack => attack.BoosterSlot))
|
||||||
|
{
|
||||||
|
var pending = group.ToList();
|
||||||
|
var booster = group.Key;
|
||||||
|
var label = booster < 0
|
||||||
|
? "Атака"
|
||||||
|
: "Атака с поддержкой: " + CardName(
|
||||||
|
CardAt(_view.You, booster)?.PrintingId ?? string.Empty);
|
||||||
|
actions.Add(new CardActionMenuItem(label, () =>
|
||||||
|
{
|
||||||
|
BeginTargetSelection(pending, "Выберите цель атаки.");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
var moves = sources.OfType<MoveIntent>().ToList();
|
||||||
|
if (moves.Count > 0)
|
||||||
|
{
|
||||||
|
actions.Add(new CardActionMenuItem("Переместить", () =>
|
||||||
|
{
|
||||||
|
BeginTargetSelection(moves, "Выберите круг для перемещения.");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var intent in sources)
|
||||||
|
{
|
||||||
|
if (intent is AttackIntent or MoveIntent)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var captured = intent;
|
||||||
|
actions.Add(new CardActionMenuItem(
|
||||||
|
Label(captured),
|
||||||
|
() => OnIntentClicked(captured)));
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.Add(new CardActionMenuItem("Подробнее", () => OpenCardDetails(card)));
|
||||||
|
|
||||||
|
if (!_board3D.TryGetCardScreenPosition(card, out var screen))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_inputLock.Acquire("card-menu");
|
||||||
|
_cardMenu.Show(
|
||||||
|
screen,
|
||||||
|
CardName(card.PrintingId),
|
||||||
|
actions,
|
||||||
|
() => _inputLock.Release("card-menu"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginTargetSelection(
|
||||||
|
IEnumerable<IntentPayload> intents,
|
||||||
|
string prompt)
|
||||||
|
{
|
||||||
|
var list = intents.ToList();
|
||||||
|
if (list.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_pendingIntents.Clear();
|
||||||
|
_pendingIntents.AddRange(list);
|
||||||
|
RebuildHighlights();
|
||||||
|
RebuildIntents(_view);
|
||||||
|
SetStatus(prompt, new Color(0.98f, 0.82f, 0.42f));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenCardDetails(BoardCardRef card)
|
||||||
|
{
|
||||||
|
if (_view == null || _overlays == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var view = FindCardView(card);
|
||||||
|
var entry = view != null
|
||||||
|
? new CardListEntry(view)
|
||||||
|
: new CardListEntry(null, true, "Закрытая карта");
|
||||||
|
_overlays.OpenCardDetail(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CardView FindCardView(BoardCardRef card)
|
||||||
|
{
|
||||||
|
if (_view == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var player = _view.Players.FirstOrDefault(item => item.Index == card.Player);
|
||||||
|
if (player == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
switch (card.Zone)
|
||||||
|
{
|
||||||
|
case BoardZone.Vanguard:
|
||||||
|
return player.Vanguard;
|
||||||
|
case BoardZone.RearGuard:
|
||||||
|
return player.RearGuards != null
|
||||||
|
&& card.Slot >= 0
|
||||||
|
&& card.Slot < player.RearGuards.Length
|
||||||
|
? player.RearGuards[card.Slot]
|
||||||
|
: null;
|
||||||
|
case BoardZone.Hand:
|
||||||
|
return player.Hand?.FirstOrDefault(item => item.InstanceId == card.InstanceId);
|
||||||
|
case BoardZone.Guardian:
|
||||||
|
return player.Guardian?.FirstOrDefault(item => item.InstanceId == card.InstanceId);
|
||||||
|
case BoardZone.Trigger:
|
||||||
|
return player.Trigger?.FirstOrDefault(item => item.InstanceId == card.InstanceId);
|
||||||
|
case BoardZone.Damage:
|
||||||
|
return player.FaceUpDamage != null
|
||||||
|
&& card.Index >= 0
|
||||||
|
&& card.Index < player.FaceUpDamage.Count
|
||||||
|
? player.FaceUpDamage[card.Index]
|
||||||
|
: null;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnBoard3DSlotClicked(BoardCardRef slot)
|
private void OnBoard3DSlotClicked(BoardCardRef slot)
|
||||||
{
|
{
|
||||||
if (_view == null || _sending)
|
if (_view == null || _sending || _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (slot.Zone == BoardZone.Guardian)
|
if (slot.Zone == BoardZone.Guardian)
|
||||||
|
|
@ -1018,7 +1227,7 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private void OnBoard3DCardDropped(BoardCardRef source, BoardCardRef target)
|
private void OnBoard3DCardDropped(BoardCardRef source, BoardCardRef target)
|
||||||
{
|
{
|
||||||
if (_view == null || _sending)
|
if (_view == null || _sending || _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
|
@ -1079,7 +1288,7 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private void OnBoard3DPileRightClicked(BoardCardRef pile)
|
private void OnBoard3DPileRightClicked(BoardCardRef pile)
|
||||||
{
|
{
|
||||||
if (_view == null)
|
if (_view == null || _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (pile.Zone == BoardZone.Soul)
|
if (pile.Zone == BoardZone.Soul)
|
||||||
|
|
@ -1088,7 +1297,7 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private void OnBoard3DCardRightClicked(BoardCardRef card)
|
private void OnBoard3DCardRightClicked(BoardCardRef card)
|
||||||
{
|
{
|
||||||
if (_view == null)
|
if (_view == null || _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (card.Zone == BoardZone.Vanguard)
|
if (card.Zone == BoardZone.Vanguard)
|
||||||
|
|
@ -1097,7 +1306,7 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private void OnBoard3DPileClicked(BoardCardRef pile)
|
private void OnBoard3DPileClicked(BoardCardRef pile)
|
||||||
{
|
{
|
||||||
if (_view == null)
|
if (_view == null || _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (pile.Zone == BoardZone.Soul)
|
if (pile.Zone == BoardZone.Soul)
|
||||||
|
|
@ -1171,6 +1380,7 @@ namespace Vanguard.UI
|
||||||
PhaseLabel(captured),
|
PhaseLabel(captured),
|
||||||
() => OnIntentClicked(captured),
|
() => OnIntentClicked(captured),
|
||||||
14);
|
14);
|
||||||
|
button.interactable = !_inputLock.IsLocked;
|
||||||
UiKit.SetPreferredHeight(button, 40f);
|
UiKit.SetPreferredHeight(button, 40f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1191,6 +1401,7 @@ namespace Vanguard.UI
|
||||||
private void BuildOverlaysAndZones()
|
private void BuildOverlaysAndZones()
|
||||||
{
|
{
|
||||||
_overlays = new OverlayNavigationService(_root, ResolveCatalogItem);
|
_overlays = new OverlayNavigationService(_root, ResolveCatalogItem);
|
||||||
|
_overlays.OpenChanged += OnOverlayOpenChanged;
|
||||||
|
|
||||||
_selfZones = new ZoneBlockStripView();
|
_selfZones = new ZoneBlockStripView();
|
||||||
_selfZones.Build(
|
_selfZones.Build(
|
||||||
|
|
@ -1214,6 +1425,88 @@ namespace Vanguard.UI
|
||||||
OpenZone(zone, _view != null ? 1 - _view.You : 1);
|
OpenZone(zone, _view != null ? 1 - _view.You : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildCombatStats()
|
||||||
|
{
|
||||||
|
_combatStats = UiKit.CreatePanel(
|
||||||
|
_root,
|
||||||
|
"CombatStats",
|
||||||
|
new Color(0.03f, 0.045f, 0.07f, 0.9f));
|
||||||
|
_combatStats.anchorMin = new Vector2(0.5f, 1f);
|
||||||
|
_combatStats.anchorMax = new Vector2(0.5f, 1f);
|
||||||
|
_combatStats.pivot = new Vector2(0.5f, 1f);
|
||||||
|
_combatStats.anchoredPosition = new Vector2(0f, -18f);
|
||||||
|
_combatStats.sizeDelta = new Vector2(620f, 48f);
|
||||||
|
var statsImage = _combatStats.GetComponent<Image>();
|
||||||
|
if (statsImage != null)
|
||||||
|
statsImage.raycastTarget = false;
|
||||||
|
|
||||||
|
_combatStatsText = UiKit.CreateText(
|
||||||
|
_combatStats,
|
||||||
|
"Text",
|
||||||
|
string.Empty,
|
||||||
|
20,
|
||||||
|
new Color(0.9f, 0.94f, 1f),
|
||||||
|
TextAnchor.MiddleCenter);
|
||||||
|
UiKit.Stretch(_combatStatsText.rectTransform);
|
||||||
|
_combatStatsText.fontStyle = FontStyle.Bold;
|
||||||
|
UiKit.MakeLiteral(_combatStatsText);
|
||||||
|
_combatStats.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCombatStats()
|
||||||
|
{
|
||||||
|
if (_combatStats == null || _combatStatsText == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var battle = _view?.Battle;
|
||||||
|
if (battle == null)
|
||||||
|
{
|
||||||
|
_combatStats.gameObject.SetActive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defenderIndex = 1 - battle.AttackerPlayer;
|
||||||
|
var attacker = BattleCard(battle.AttackerPlayer, battle.AttackerSlot);
|
||||||
|
var defender = battle.InterceptorSlot >= 0
|
||||||
|
? BattleCard(defenderIndex, battle.InterceptorSlot)
|
||||||
|
: BattleCard(defenderIndex, battle.TargetSlot);
|
||||||
|
defender ??= BattleCard(defenderIndex, battle.TargetSlot);
|
||||||
|
|
||||||
|
if (attacker == null || defender == null)
|
||||||
|
{
|
||||||
|
_combatStats.gameObject.SetActive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attackPower = attacker.Power;
|
||||||
|
var shield = defender.Power + battle.GuardShield;
|
||||||
|
var text = battle.Nullified
|
||||||
|
? "POWER " + attackPower + " · NULLIFIED"
|
||||||
|
: "POWER " + attackPower + " vs SHIELD " + shield;
|
||||||
|
_combatStatsText.text = text;
|
||||||
|
_combatStatsText.color = battle.Nullified
|
||||||
|
? new Color(0.7f, 0.76f, 0.9f)
|
||||||
|
: attackPower >= shield
|
||||||
|
? new Color(1f, 0.72f, 0.32f)
|
||||||
|
: new Color(0.52f, 0.82f, 1f);
|
||||||
|
_combatStats.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CardView BattleCard(int playerIndex, int slot)
|
||||||
|
{
|
||||||
|
var player = _view?.Players.FirstOrDefault(item => item.Index == playerIndex);
|
||||||
|
if (player == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (slot < 0)
|
||||||
|
return player.Vanguard;
|
||||||
|
|
||||||
|
return player.RearGuards != null
|
||||||
|
&& slot < player.RearGuards.Length
|
||||||
|
? player.RearGuards[slot]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateZoneStrips()
|
private void UpdateZoneStrips()
|
||||||
{
|
{
|
||||||
if (_view == null)
|
if (_view == null)
|
||||||
|
|
@ -1241,6 +1534,9 @@ namespace Vanguard.UI
|
||||||
_catalog.TryGetValue(printingId, out var item) ? item : null;
|
_catalog.TryGetValue(printingId, out var item) ? item : null;
|
||||||
private void OnDamageClicked(int displayIndex)
|
private void OnDamageClicked(int displayIndex)
|
||||||
{
|
{
|
||||||
|
if (_inputLock.IsLocked)
|
||||||
|
return;
|
||||||
|
|
||||||
if (IsDamageChoice())
|
if (IsDamageChoice())
|
||||||
{
|
{
|
||||||
if (_view?.Choice?.Options == null)
|
if (_view?.Choice?.Options == null)
|
||||||
|
|
@ -1317,7 +1613,8 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private void SendMulligan()
|
private void SendMulligan()
|
||||||
{
|
{
|
||||||
if (!IsMulliganSelecting() || _mulliganSelected.Count == 0)
|
if (!IsMulliganSelecting() || _mulliganSelected.Count == 0
|
||||||
|
|| _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var indexes = _mulliganSelected.OrderBy(index => index).ToList();
|
var indexes = _mulliganSelected.OrderBy(index => index).ToList();
|
||||||
|
|
@ -1330,7 +1627,7 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private void OnBoardTargetClicked(BoardTarget target)
|
private void OnBoardTargetClicked(BoardTarget target)
|
||||||
{
|
{
|
||||||
if (_view == null || _sending)
|
if (_view == null || _sending || _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (IsMulliganSelecting() && target.IsHand && target.Player == _view.You)
|
if (IsMulliganSelecting() && target.IsHand && target.Player == _view.You)
|
||||||
|
|
@ -1787,6 +2084,7 @@ namespace Vanguard.UI
|
||||||
Label(captured),
|
Label(captured),
|
||||||
() => OnIntentClicked(captured),
|
() => OnIntentClicked(captured),
|
||||||
16);
|
16);
|
||||||
|
button.interactable = !_inputLock.IsLocked;
|
||||||
UiKit.SetPreferredHeight(button, 46f);
|
UiKit.SetPreferredHeight(button, 46f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,13 @@ namespace Vanguard.UI
|
||||||
[" карт."] = " cards.",
|
[" карт."] = " cards.",
|
||||||
[" пыли)"] = " dust)",
|
[" пыли)"] = " dust)",
|
||||||
["Атака "] = "Attack ",
|
["Атака "] = "Attack ",
|
||||||
|
["Атака"] = "Attack",
|
||||||
|
["Атака с поддержкой: "] = "Boosted attack: ",
|
||||||
|
["Переместить"] = "Move",
|
||||||
|
["Подробнее"] = "Details",
|
||||||
|
["Нет доступных действий"] = "No available actions",
|
||||||
|
["Выберите цель атаки."] = "Choose an attack target.",
|
||||||
|
["Выберите круг для перемещения."] = "Choose a circle to move to.",
|
||||||
["В меню"] = "To menu",
|
["В меню"] = "To menu",
|
||||||
["Готово"] = "Done",
|
["Готово"] = "Done",
|
||||||
["Журнал"] = "Log",
|
["Журнал"] = "Log",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace Vanguard.UI.Overlays
|
||||||
|
{
|
||||||
|
public sealed class CardActionMenuItem
|
||||||
|
{
|
||||||
|
public CardActionMenuItem(string label, Action action, bool enabled = true)
|
||||||
|
{
|
||||||
|
Label = label;
|
||||||
|
Action = action;
|
||||||
|
Enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Label { get; }
|
||||||
|
public Action Action { get; }
|
||||||
|
public bool Enabled { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Экранное контекстное меню над картой.</summary>
|
||||||
|
public sealed class CardActionMenuView
|
||||||
|
{
|
||||||
|
private RectTransform _root;
|
||||||
|
private RectTransform _popup;
|
||||||
|
private Action _onClose;
|
||||||
|
|
||||||
|
public bool IsVisible => _root != null && _root.gameObject.activeSelf;
|
||||||
|
|
||||||
|
public void Build(Transform parent)
|
||||||
|
{
|
||||||
|
_root = UiKit.CreatePanel(parent, "CardActionMenu", new Color(0f, 0f, 0f, 0.01f));
|
||||||
|
var image = _root.GetComponent<Image>();
|
||||||
|
image.raycastTarget = true;
|
||||||
|
|
||||||
|
var backdrop = _root.gameObject.AddComponent<Button>();
|
||||||
|
backdrop.targetGraphic = image;
|
||||||
|
backdrop.onClick.AddListener(Hide);
|
||||||
|
|
||||||
|
_popup = UiKit.CreatePanel(_root, "Popup", new Color(0.045f, 0.06f, 0.09f, 0.98f));
|
||||||
|
_popup.anchorMin = new Vector2(0f, 1f);
|
||||||
|
_popup.anchorMax = new Vector2(0f, 1f);
|
||||||
|
_popup.pivot = new Vector2(0f, 1f);
|
||||||
|
_popup.sizeDelta = new Vector2(250f, 380f);
|
||||||
|
UiKit.AddVerticalLayout(_popup, 8, 4);
|
||||||
|
|
||||||
|
_root.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Show(
|
||||||
|
Vector2 screenPosition,
|
||||||
|
string title,
|
||||||
|
IReadOnlyList<CardActionMenuItem> items,
|
||||||
|
Action onClose)
|
||||||
|
{
|
||||||
|
if (_root == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_onClose = onClose;
|
||||||
|
ClearChildren(_popup);
|
||||||
|
|
||||||
|
var heading = UiKit.CreateText(
|
||||||
|
_popup,
|
||||||
|
"Title",
|
||||||
|
title,
|
||||||
|
15,
|
||||||
|
new Color(0.86f, 0.91f, 1f),
|
||||||
|
TextAnchor.MiddleLeft);
|
||||||
|
heading.fontStyle = FontStyle.Bold;
|
||||||
|
UiKit.MakeLiteral(heading);
|
||||||
|
UiKit.SetPreferredHeight(heading, 26f);
|
||||||
|
|
||||||
|
if (items == null || items.Count == 0)
|
||||||
|
{
|
||||||
|
var empty = UiKit.CreateText(
|
||||||
|
_popup,
|
||||||
|
"Empty",
|
||||||
|
"Нет доступных действий",
|
||||||
|
13,
|
||||||
|
new Color(0.62f, 0.68f, 0.82f));
|
||||||
|
UiKit.SetPreferredHeight(empty, 34f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
var button = UiKit.CreateButton(
|
||||||
|
_popup,
|
||||||
|
"Action_" + item.Label,
|
||||||
|
item.Label,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var action = item.Action;
|
||||||
|
Hide();
|
||||||
|
action?.Invoke();
|
||||||
|
},
|
||||||
|
14);
|
||||||
|
button.interactable = item.Enabled;
|
||||||
|
UiKit.SetPreferredHeight(button, 36f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_root.SetAsLastSibling();
|
||||||
|
_root.gameObject.SetActive(true);
|
||||||
|
|
||||||
|
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||||
|
_root,
|
||||||
|
screenPosition,
|
||||||
|
null,
|
||||||
|
out var local))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rect = _root.rect;
|
||||||
|
var fromLeft = local.x + rect.width * 0.5f;
|
||||||
|
var fromTop = rect.height * 0.5f - local.y;
|
||||||
|
var x = Mathf.Clamp(
|
||||||
|
fromLeft + 18f,
|
||||||
|
8f,
|
||||||
|
Mathf.Max(8f, rect.width - 258f));
|
||||||
|
var y = Mathf.Clamp(
|
||||||
|
-fromTop - 18f,
|
||||||
|
-Mathf.Max(8f, rect.height - 388f),
|
||||||
|
-8f);
|
||||||
|
_popup.anchoredPosition = new Vector2(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Hide()
|
||||||
|
{
|
||||||
|
if (_root == null || !_root.gameObject.activeSelf)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_root.gameObject.SetActive(false);
|
||||||
|
var callback = _onClose;
|
||||||
|
_onClose = null;
|
||||||
|
callback?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClearChildren(Transform parent)
|
||||||
|
{
|
||||||
|
for (var i = parent.childCount - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var child = parent.GetChild(i).gameObject;
|
||||||
|
child.SetActive(false);
|
||||||
|
if (Application.isPlaying)
|
||||||
|
UnityEngine.Object.Destroy(child);
|
||||||
|
else
|
||||||
|
UnityEngine.Object.DestroyImmediate(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 11b04f902000e5049a3dbc457cd1f570
|
||||||
|
|
@ -46,8 +46,25 @@ namespace Vanguard.UI.Overlays
|
||||||
_detail.Hide();
|
_detail.Hide();
|
||||||
_list.Show(request, OpenDetail, CloseAll);
|
_list.Show(request, OpenDetail, CloseAll);
|
||||||
_root.SetAsLastSibling();
|
_root.SetAsLastSibling();
|
||||||
|
var wasOpen = _root.gameObject.activeSelf;
|
||||||
_root.gameObject.SetActive(true);
|
_root.gameObject.SetActive(true);
|
||||||
OpenChanged?.Invoke(true);
|
if (!wasOpen)
|
||||||
|
OpenChanged?.Invoke(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenCardDetail(CardListEntry entry)
|
||||||
|
{
|
||||||
|
if (entry == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_currentRequest = null;
|
||||||
|
_list.Hide();
|
||||||
|
_detail.Show(entry, CloseAll);
|
||||||
|
_root.SetAsLastSibling();
|
||||||
|
var wasOpen = _root.gameObject.activeSelf;
|
||||||
|
_root.gameObject.SetActive(true);
|
||||||
|
if (!wasOpen)
|
||||||
|
OpenChanged?.Invoke(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenDetail(CardListEntry entry)
|
private void OpenDetail(CardListEntry entry)
|
||||||
|
|
@ -72,11 +89,13 @@ namespace Vanguard.UI.Overlays
|
||||||
|
|
||||||
public void CloseAll()
|
public void CloseAll()
|
||||||
{
|
{
|
||||||
|
var wasOpen = _root != null && _root.gameObject.activeSelf;
|
||||||
_list?.Hide();
|
_list?.Hide();
|
||||||
_detail?.Hide();
|
_detail?.Hide();
|
||||||
_root.gameObject.SetActive(false);
|
_root.gameObject.SetActive(false);
|
||||||
_currentRequest = null;
|
_currentRequest = null;
|
||||||
OpenChanged?.Invoke(false);
|
if (wasOpen)
|
||||||
|
OpenChanged?.Invoke(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,79 @@ namespace Vanguard.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Animation_Config_Asset_Is_Loaded_From_Resources()
|
||||||
|
{
|
||||||
|
var asset = Resources.Load<Vanguard.Battle.AnimationConfig>("AnimationConfig");
|
||||||
|
Assert.IsNotNull(asset);
|
||||||
|
Assert.Greater(asset.cardMoveDuration, 0f);
|
||||||
|
Assert.Greater(asset.arrowShowDuration, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Input_Lock_Returns_After_Last_Reason()
|
||||||
|
{
|
||||||
|
var lockService = new Vanguard.Battle.InputLockService();
|
||||||
|
var changes = new List<bool>();
|
||||||
|
lockService.Changed += changes.Add;
|
||||||
|
|
||||||
|
lockService.Acquire("board");
|
||||||
|
lockService.Acquire("feedback");
|
||||||
|
Assert.IsTrue(lockService.IsLocked);
|
||||||
|
|
||||||
|
lockService.Release("board");
|
||||||
|
Assert.IsTrue(lockService.IsLocked);
|
||||||
|
|
||||||
|
lockService.Release("feedback");
|
||||||
|
Assert.IsFalse(lockService.IsLocked);
|
||||||
|
CollectionAssert.AreEqual(new[] { true, false }, changes);
|
||||||
|
|
||||||
|
lockService.Acquire("overlay");
|
||||||
|
lockService.Acquire("overlay");
|
||||||
|
lockService.Release("overlay");
|
||||||
|
Assert.IsTrue(lockService.IsLocked);
|
||||||
|
lockService.Release("overlay");
|
||||||
|
Assert.IsFalse(lockService.IsLocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Card_Action_Menu_Invokes_Action_And_Closes()
|
||||||
|
{
|
||||||
|
var canvas = UiKit.CreateCanvas("Menu Test Canvas");
|
||||||
|
var menu = new CardActionMenuView();
|
||||||
|
menu.Build(canvas.transform);
|
||||||
|
var invoked = false;
|
||||||
|
var closed = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
menu.Show(
|
||||||
|
new Vector2(200f, 200f),
|
||||||
|
"Карта",
|
||||||
|
new List<CardActionMenuItem>
|
||||||
|
{
|
||||||
|
new CardActionMenuItem("Тест", () => invoked = true),
|
||||||
|
},
|
||||||
|
() => closed = true);
|
||||||
|
|
||||||
|
Assert.IsTrue(menu.IsVisible);
|
||||||
|
var button = Resources.FindObjectsOfTypeAll<Button>()
|
||||||
|
.FirstOrDefault(item => item.name == "Action_Тест");
|
||||||
|
Assert.IsNotNull(button);
|
||||||
|
button.onClick.Invoke();
|
||||||
|
Assert.IsTrue(invoked);
|
||||||
|
Assert.IsTrue(closed);
|
||||||
|
Assert.IsFalse(menu.IsVisible);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (canvas != null)
|
||||||
|
Object.DestroyImmediate(canvas.gameObject);
|
||||||
|
var eventSystem = Object.FindAnyObjectByType<EventSystem>();
|
||||||
|
if (eventSystem != null)
|
||||||
|
Object.DestroyImmediate(eventSystem.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static CardView Card(string id, int instanceId) => new CardView
|
private static CardView Card(string id, int instanceId) => new CardView
|
||||||
{
|
{
|
||||||
PrintingId = id,
|
PrintingId = id,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,32 @@ namespace Vanguard.Tests
|
||||||
{
|
{
|
||||||
public sealed class GameBoard3DTests
|
public sealed class GameBoard3DTests
|
||||||
{
|
{
|
||||||
|
[Test]
|
||||||
|
public void Board_Shows_Attack_Arrow_For_Active_Battle()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var board = GameBoard3D.Create(null, config);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
board.SetVisible(true);
|
||||||
|
var view = BuildView(you: 0);
|
||||||
|
view.Battle = new BattleView
|
||||||
|
{
|
||||||
|
AttackerPlayer = 0,
|
||||||
|
AttackerSlot = 3,
|
||||||
|
TargetSlot = -1,
|
||||||
|
GuardShield = 5000,
|
||||||
|
};
|
||||||
|
board.Render(view, new BoardRenderState());
|
||||||
|
|
||||||
|
Assert.IsTrue(board.AttackArrowVisible);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Object.DestroyImmediate(board.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Board_Renders_Field_Hand_Guardian_Damage_And_Soul()
|
public void Board_Renders_Field_Hand_Guardian_Damage_And_Soul()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -911,3 +911,36 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
- Дальше по плану: Phase 2 — `InputLockService` + `AnimationConfig` +
|
- Дальше по плану: Phase 2 — `InputLockService` + `AnimationConfig` +
|
||||||
контекстное меню; Phase 3 — Power/Shield и стрелка; Phase 4 — Guardian
|
контекстное меню; Phase 3 — Power/Shield и стрелка; Phase 4 — Guardian
|
||||||
стопка, Trigger mirror, debug-подписи.
|
стопка, Trigger mirror, debug-подписи.
|
||||||
|
|
||||||
|
## 2026-10-23 — UX Phase 2–4: ввод, контекст, бой и раскладка
|
||||||
|
|
||||||
|
- `InputLockService` — reference-count по причинам (`feedback`, `board`,
|
||||||
|
`overlay`, `card-menu`); ввод возвращается только после последней причины.
|
||||||
|
`AnimationConfig` (ScriptableObject в `Resources/AnimationConfig.asset`):
|
||||||
|
скорости карт, длительности фидбека/флипа/retire, хвост блокировки и
|
||||||
|
стрелка. `Card3D` получает скорости из конфига, `GameBoard3D` блокирует
|
||||||
|
доску на время снапа/переворота, `BattleView` — на время очереди событий
|
||||||
|
и открытых оверлеев. Кнопки интентов/фаз и клики по столу не проходят,
|
||||||
|
пока идёт анимация; «Пропустить анимации» остаётся доступной.
|
||||||
|
- Контекстное меню `CardActionMenuView`: ЛКМ по своей карте V/R открывает
|
||||||
|
список действий, отфильтрованных по `LegalIntents` (атака, атака с
|
||||||
|
поддержкой, перемещение, способность, подробнее). Действия с целями
|
||||||
|
переводят в режим выбора цели; карты противника и зоны открывают
|
||||||
|
`CardDetailOverlay`/списки. Меню закрывается на новом снимке и держит
|
||||||
|
`card-menu` в InputLock.
|
||||||
|
- Боевая плашка `POWER X vs SHIELD Y` в верхней части экрана: сила
|
||||||
|
атакующего против силы защищающегося (с учётом перехватчика) плюс
|
||||||
|
`GuardShield`; пересчитывается на каждом снимке, `NULLIFIED` показывается
|
||||||
|
отдельно. `AttackArrowView` рисует затухающую 3D-стрелку с наконечником от
|
||||||
|
атакующего к цели; точки обновляются каждый кадр по трансформам карт,
|
||||||
|
поэтому стрелка едет за анимацией.
|
||||||
|
- Guardian-карты повёрнуты на 90° вокруг Y, уложены с перекрытием около 30%
|
||||||
|
(spacing 0.62 при высоте 0.88), новые сверху за счёт микро-подъёма.
|
||||||
|
Trigger-зона зеркальна по X и ориентации; 3D-подписи `DECK`/`DROP`/
|
||||||
|
`TRIGGER` скрыты флагом `showZoneLabels` (по умолчанию false).
|
||||||
|
- Проверки: клиент EditMode **26/26** (InputLock, контекстное меню, стрелка
|
||||||
|
атаки, загрузка `AnimationConfig`), `SCENE_SMOKE success=True`,
|
||||||
|
`PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||||||
|
- Дальше: по желанию вынести overlay-экраны и меню в prefab-ассеты; проверить
|
||||||
|
итоговые позиции на реальном разрешении и, при необходимости, подкрутить
|
||||||
|
`AnimationConfig`/`PlaymatLayoutConfig`.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Vanguard ReMaster — состояние сессии
|
# Vanguard ReMaster — состояние сессии
|
||||||
|
|
||||||
**Дата:** 2026-10-22
|
**Дата:** 2026-10-23
|
||||||
|
|
||||||
**HEAD:** см. `git log -1`
|
**HEAD:** см. `git log -1`
|
||||||
**Коротко:** сервер готов, клиент — рабочий каркас с 3D-сценами.
|
**Коротко:** сервер готов, клиент — рабочий каркас с 3D-сценами.
|
||||||
|
|
@ -96,11 +96,21 @@
|
||||||
- Маллиган: один раз на игрока, оба игрока; выбранные карты уходят в низ
|
- Маллиган: один раз на игрока, оба игрока; выбранные карты уходят в низ
|
||||||
колоды, добираются новые, затем shuffle. Клик по руке выбирает карты.
|
колоды, добираются новые, затем shuffle. Клик по руке выбирает карты.
|
||||||
- Анимации: event-фидбек (атака, guard, drive, урон, retire, heal) поверх
|
- Анимации: event-фидбек (атака, guard, drive, урон, retire, heal) поверх
|
||||||
стола, не блокирует ввод; очередь событий + кнопка «Пропустить анимации».
|
стола; ввод заблокирован до завершения через `InputLockService`, очередь
|
||||||
Guard/drive/intercept показывают имя карты и держатся 1.5–2 с, чтобы
|
событий + кнопка «Пропустить анимации». Длительности и скорости карт —
|
||||||
успеть прочитать защиту бота. 3D-карты плавно снапятся, поворачиваются
|
`AnimationConfig` (`Resources/AnimationConfig`). Guard/drive/intercept
|
||||||
в Rest и переворачиваются; подсветка целей и точки броска обновляются
|
показывают имя карты и держатся 1.5–2 с. 3D-карты плавно снапятся,
|
||||||
на каждом снимке.
|
поворачиваются в Rest и переворачиваются; подсветка целей и точки броска
|
||||||
|
обновляются на каждом снимке.
|
||||||
|
- Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против
|
||||||
|
Power защищающегося + GuardShield), пересчитывается на каждом снимке, в
|
||||||
|
том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,
|
||||||
|
которая следует за картами. ЛКМ по своей карте V/R открывает контекстное
|
||||||
|
меню с действиями из `LegalIntents` (атака/boost, перемещение, способность,
|
||||||
|
подробнее); карты противника и зоны открывают детали/списки. ПКМ по VC —
|
||||||
|
душа. Guardian-карты повёрнуты на 90° и уложены с перекрытием ~30%,
|
||||||
|
новые сверху; Trigger-зона зеркальна у противника. 3D-подписи зон скрыты,
|
||||||
|
включаются флагом `showZoneLabels`.
|
||||||
- Реальные арты: `imageKey` из `/api/cards` + `CardImageService` ищет
|
- Реальные арты: `imageKey` из `/api/cards` + `CardImageService` ищет
|
||||||
`Resources/CardArt/{imageKey}`. Скачаны все 124 арта. Импорт:
|
`Resources/CardArt/{imageKey}`. Скачаны все 124 арта. Импорт:
|
||||||
`tools/import_card_art.py`.
|
`tools/import_card_art.py`.
|
||||||
|
|
@ -152,11 +162,12 @@ curl http://127.0.0.1:18080/health
|
||||||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||||
```
|
```
|
||||||
|
|
||||||
Клиентские проверки: **22/22** EditMode (сборка всех экранов, импорт арта,
|
Клиентские проверки: **26/26** EditMode (сборка всех экранов, импорт арта,
|
||||||
перевод статических/динамических строк, live-переключение языка, рендер
|
перевод статических/динамических строк, live-переключение языка, рендер
|
||||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||||
CardListView/Detail, зоны и рука противника), `SCENE_SMOKE success=True`,
|
CardListView/Detail, зоны и рука противника, InputLock, контекстное меню,
|
||||||
|
стрелка атаки), `SCENE_SMOKE success=True`,
|
||||||
`PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
`PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||||||
|
|
||||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||||
|
|
@ -189,6 +200,11 @@ client/Assets/Vanguard/Runtime/UI/Overlays/
|
||||||
CardDetailOverlay.cs крупная карта и текст
|
CardDetailOverlay.cs крупная карта и текст
|
||||||
CardListModels.cs зоны и скрытые карты
|
CardListModels.cs зоны и скрытые карты
|
||||||
ZoneBlockStripView.cs кликабельные блоки зон
|
ZoneBlockStripView.cs кликабельные блоки зон
|
||||||
|
CardActionMenuView.cs контекстное меню карты
|
||||||
|
client/Assets/Vanguard/Runtime/Battle/
|
||||||
|
AnimationConfig.cs длительности/скорости анимаций
|
||||||
|
InputLockService.cs блокировка ввода по причинам
|
||||||
|
GameConnection.cs
|
||||||
client/Assets/Vanguard/Runtime/Board3D/
|
client/Assets/Vanguard/Runtime/Board3D/
|
||||||
Config/PlaymatLayoutConfig.cs ScriptableObject-раскладка
|
Config/PlaymatLayoutConfig.cs ScriptableObject-раскладка
|
||||||
Data/BoardTypes.cs зоны, круги, CardState3D, refs
|
Data/BoardTypes.cs зоны, круги, CardState3D, refs
|
||||||
|
|
@ -196,10 +212,11 @@ client/Assets/Vanguard/Runtime/Board3D/
|
||||||
Logic/BoardPlacementRules.cs Ride/Call/Move/Guard
|
Logic/BoardPlacementRules.cs Ride/Call/Move/Guard
|
||||||
View/GameBoard3D.cs стол, круги, пул карт, drag&drop
|
View/GameBoard3D.cs стол, круги, пул карт, drag&drop
|
||||||
View/Card3D.cs карта: лицо/рубашка/торец, Rest, флип
|
View/Card3D.cs карта: лицо/рубашка/торец, Rest, флип
|
||||||
|
View/AttackArrowView.cs стрелка атаки с наконечником
|
||||||
PlaymatSandbox.cs демо без сервера
|
PlaymatSandbox.cs демо без сервера
|
||||||
|
client/Assets/Vanguard/Resources/AnimationConfig.asset настройки анимаций
|
||||||
client/Assets/Vanguard/Resources/CardArt/vanguard_back.png рубашка VF
|
client/Assets/Vanguard/Resources/CardArt/vanguard_back.png рубашка VF
|
||||||
client/Assets/Vanguard/Scenes/GameBoard3D.unity песочница поля
|
client/Assets/Vanguard/Scenes/GameBoard3D.unity песочница поля
|
||||||
client/Assets/Vanguard/Runtime/Battle/GameConnection.cs
|
|
||||||
client/Assets/Vanguard/Runtime/Api/ApiClient.cs + ApiContracts.cs
|
client/Assets/Vanguard/Runtime/Api/ApiClient.cs + ApiContracts.cs
|
||||||
client/Assets/Vanguard/Editor/SceneBuilder.cs + SceneSmoke.cs
|
client/Assets/Vanguard/Editor/SceneBuilder.cs + SceneSmoke.cs
|
||||||
client/Assets/Vanguard/Scenes/*.unity
|
client/Assets/Vanguard/Scenes/*.unity
|
||||||
|
|
@ -221,8 +238,10 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
|
||||||
|
|
||||||
## Следующие шаги
|
## Следующие шаги
|
||||||
|
|
||||||
1. UX-план 3D-поля: Phase 2 — InputLock/AnimationConfig и контекстное меню;
|
1. UX-план 3D-поля: Phase 2–4 закрыты (InputLock/AnimationConfig, контекстное
|
||||||
затем Power/Shield + стрелка, затем Guardian/Trigger/подписи.
|
меню, Power/Shield, стрелка, Guardian/Trigger, debug-подписи). Осталось:
|
||||||
|
при необходимости вынести overlay-экраны в prefab-ассеты и отполировать
|
||||||
|
расположение на реальном разрешении.
|
||||||
2. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты.
|
2. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты.
|
||||||
3. 3D-наполнение: модели карт/стола, prefab-экраны, анимации retire/ghost.
|
3. 3D-наполнение: модели карт/стола, prefab-экраны, анимации retire/ghost.
|
||||||
4. Звук и настройки, DOTween.
|
4. Звук и настройки, DOTween.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue