Build 3D playmat with drag and drop
CI / test (push) Successful in 3m58s
Details
CI / test (push) Successful in 3m58s
Details
This commit is contained in:
parent
e23116946c
commit
657859fec1
|
|
@ -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.Board3D;
|
||||||
|
|
||||||
namespace Vanguard.Editor
|
namespace Vanguard.Editor
|
||||||
{
|
{
|
||||||
|
|
@ -17,6 +18,9 @@ namespace Vanguard.Editor
|
||||||
{
|
{
|
||||||
private const string SceneDir = "Assets/Vanguard/Scenes";
|
private const string SceneDir = "Assets/Vanguard/Scenes";
|
||||||
private const string BootPath = "Assets/Scenes/Boot.unity";
|
private const string BootPath = "Assets/Scenes/Boot.unity";
|
||||||
|
private const string PlaymatConfigPath =
|
||||||
|
"Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset";
|
||||||
|
private const string PlaymatScenePath = SceneDir + "/GameBoard3D.unity";
|
||||||
|
|
||||||
[MenuItem("Vanguard/Rebuild Client Scenes")]
|
[MenuItem("Vanguard/Rebuild Client Scenes")]
|
||||||
public static void BuildAll()
|
public static void BuildAll()
|
||||||
|
|
@ -31,6 +35,7 @@ namespace Vanguard.Editor
|
||||||
Create<LobbySceneController>("Lobby");
|
Create<LobbySceneController>("Lobby");
|
||||||
Create<DeckBuilderSceneController>("DeckBuilder");
|
Create<DeckBuilderSceneController>("DeckBuilder");
|
||||||
Create<BattleSceneController>("Battle");
|
Create<BattleSceneController>("Battle");
|
||||||
|
EnsurePlaymatConfig();
|
||||||
|
|
||||||
var scenes = new List<EditorBuildSettingsScene>();
|
var scenes = new List<EditorBuildSettingsScene>();
|
||||||
Add(scenes, BootPath);
|
Add(scenes, BootPath);
|
||||||
|
|
@ -46,6 +51,66 @@ namespace Vanguard.Editor
|
||||||
Debug.Log("Vanguard scenes rebuilt: " + scenes.Count);
|
Debug.Log("Vanguard scenes rebuilt: " + scenes.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MenuItem("Vanguard/Rebuild Playmat Sandbox")]
|
||||||
|
public static void BuildPlaymatSandbox()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(SceneDir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(SceneDir);
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = EnsurePlaymatConfig();
|
||||||
|
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
|
||||||
|
var cameraGo = new GameObject("Main Camera");
|
||||||
|
cameraGo.tag = "MainCamera";
|
||||||
|
var camera = cameraGo.AddComponent<Camera>();
|
||||||
|
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||||
|
camera.backgroundColor = new Color(0.02f, 0.025f, 0.045f);
|
||||||
|
camera.fieldOfView = config.cameraFov;
|
||||||
|
camera.transform.position = config.cameraPosition;
|
||||||
|
var direction = config.cameraTarget - config.cameraPosition;
|
||||||
|
if (direction.sqrMagnitude > 0.001f)
|
||||||
|
camera.transform.rotation = Quaternion.LookRotation(direction, Vector3.up);
|
||||||
|
|
||||||
|
var lightGo = new GameObject("Directional Light");
|
||||||
|
var light = lightGo.AddComponent<Light>();
|
||||||
|
light.type = LightType.Directional;
|
||||||
|
light.color = new Color(1f, 0.88f, 0.68f);
|
||||||
|
light.intensity = 1.15f;
|
||||||
|
light.shadows = LightShadows.Soft;
|
||||||
|
light.transform.rotation = Quaternion.Euler(48f, -32f, 0f);
|
||||||
|
|
||||||
|
var root = new GameObject("Playmat Sandbox");
|
||||||
|
var sandbox = root.AddComponent<PlaymatSandbox>();
|
||||||
|
sandbox.config = config;
|
||||||
|
|
||||||
|
EditorSceneManager.SaveScene(scene, PlaymatScenePath);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
Debug.Log("Vanguard playmat sandbox rebuilt: " + PlaymatScenePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlaymatLayoutConfig EnsurePlaymatConfig()
|
||||||
|
{
|
||||||
|
var config = AssetDatabase.LoadAssetAtPath<PlaymatLayoutConfig>(PlaymatConfigPath);
|
||||||
|
if (config != null)
|
||||||
|
return config;
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(PlaymatConfigPath);
|
||||||
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
AssetDatabase.CreateAsset(config, PlaymatConfigPath);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
private static void Create<T>(string sceneName) where T : MonoBehaviour
|
private static void Create<T>(string sceneName) where T : MonoBehaviour
|
||||||
{
|
{
|
||||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,40 @@ namespace Vanguard.Editor
|
||||||
EditorApplication.EnterPlaymode();
|
EditorApplication.EnterPlaymode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MenuItem("Vanguard/Play Playmat Scene Smoke")]
|
||||||
|
public static void RunPlaymat()
|
||||||
|
{
|
||||||
|
EditorSceneManager.OpenScene("Assets/Vanguard/Scenes/GameBoard3D.unity");
|
||||||
|
_started = EditorApplication.timeSinceStartup;
|
||||||
|
_checks = 0;
|
||||||
|
EditorApplication.update += TickPlaymat;
|
||||||
|
EditorApplication.EnterPlaymode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TickPlaymat()
|
||||||
|
{
|
||||||
|
if (!EditorApplication.isPlaying)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_checks++;
|
||||||
|
if (EditorApplication.timeSinceStartup - _started < 5.0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
EditorApplication.update -= TickPlaymat;
|
||||||
|
|
||||||
|
var board = UnityEngine.Object.FindAnyObjectByType<Vanguard.Board3D.GameBoard3D>();
|
||||||
|
var cards = UnityEngine.Object.FindObjectsByType<Vanguard.Board3D.Card3D>(
|
||||||
|
FindObjectsInactive.Exclude);
|
||||||
|
var success = board != null && cards.Length >= 10;
|
||||||
|
Debug.Log(
|
||||||
|
"PLAYMAT_SMOKE success=" + success
|
||||||
|
+ " board=" + (board != null)
|
||||||
|
+ " cards=" + cards.Length
|
||||||
|
+ " error=" + VanguardApp.LastInitializationError);
|
||||||
|
|
||||||
|
EditorApplication.Exit(success ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
private static void Tick()
|
private static void Tick()
|
||||||
{
|
{
|
||||||
if (!EditorApplication.isPlaying)
|
if (!EditorApplication.isPlaying)
|
||||||
|
|
|
||||||
|
|
@ -51,15 +51,15 @@ namespace Vanguard.App
|
||||||
|
|
||||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||||
camera.backgroundColor = palette.Background;
|
camera.backgroundColor = palette.Background;
|
||||||
camera.fieldOfView = 52f;
|
camera.fieldOfView = theme == SceneTheme.Battle ? 50f : 52f;
|
||||||
camera.transform.position = theme switch
|
camera.transform.position = theme switch
|
||||||
{
|
{
|
||||||
SceneTheme.Battle => new Vector3(0f, 7.5f, -13f),
|
SceneTheme.Battle => new Vector3(0f, 8.7f, -12.4f),
|
||||||
SceneTheme.Login => new Vector3(0f, 2.4f, -9.5f),
|
SceneTheme.Login => new Vector3(0f, 2.4f, -9.5f),
|
||||||
_ => new Vector3(0f, 3.2f, -10f),
|
_ => new Vector3(0f, 3.2f, -10f),
|
||||||
};
|
};
|
||||||
camera.transform.rotation = theme == SceneTheme.Battle
|
camera.transform.rotation = theme == SceneTheme.Battle
|
||||||
? Quaternion.Euler(28f, 0f, 0f)
|
? Quaternion.Euler(34.4f, 0f, 0f)
|
||||||
: Quaternion.Euler(8f, 0f, 0f);
|
: Quaternion.Euler(8f, 0f, 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,14 +149,11 @@ namespace Vanguard.App
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SceneTheme.Battle:
|
case SceneTheme.Battle:
|
||||||
Cube(root, "Battle Table", new Vector3(0f, 0.2f, 1.5f), new Vector3(13f, 0.4f, 7f), palette.Structure);
|
// Сам стол и круги строит GameBoard3D. Здесь только фон.
|
||||||
Cube(root, "Center Line", new Vector3(0f, 0.44f, 1.5f), new Vector3(13f, 0.04f, 0.12f), palette.Accent, 1.2f);
|
Cube(root, "Back Wall", new Vector3(0f, 4.2f, 9.5f), new Vector3(24f, 8.4f, 0.5f), palette.Structure);
|
||||||
for (var i = 0; i < 5; i++)
|
Cube(root, "Pillar L", new Vector3(-8.4f, 2.5f, 4.4f), new Vector3(0.8f, 5f, 0.8f), palette.Structure);
|
||||||
{
|
Cube(root, "Pillar R", new Vector3(8.4f, 2.5f, 4.4f), new Vector3(0.8f, 5f, 0.8f), palette.Structure);
|
||||||
var x = (i - 2) * 2.2f;
|
Cube(root, "Back Light", new Vector3(0f, 5.4f, 9f), new Vector3(6.5f, 0.16f, 0.8f), palette.Accent, 1.8f);
|
||||||
Cube(root, "Zone " + i, new Vector3(x, 0.44f, 3.1f), new Vector3(1.8f, 0.04f, 2.2f), palette.Zone, 0.5f);
|
|
||||||
Cube(root, "Enemy Zone " + i, new Vector3(x, 0.44f, -0.1f), new Vector3(1.8f, 0.04f, 2.2f), palette.EnemyZone, 0.5f);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ namespace Vanguard.App
|
||||||
public const string Lobby = "Lobby";
|
public const string Lobby = "Lobby";
|
||||||
public const string DeckBuilder = "DeckBuilder";
|
public const string DeckBuilder = "DeckBuilder";
|
||||||
public const string Battle = "Battle";
|
public const string Battle = "Battle";
|
||||||
|
public const string PlaymatSandbox = "GameBoard3D";
|
||||||
|
|
||||||
public DeckDto PendingQueueDeck { get; private set; }
|
public DeckDto PendingQueueDeck { get; private set; }
|
||||||
public MatchConnectionInfo PendingMatch { get; private set; }
|
public MatchConnectionInfo PendingMatch { get; private set; }
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,12 @@ namespace Vanguard.App
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
if (Flow != null)
|
if (Flow != null
|
||||||
|
&& UnityEngine.SceneManagement.SceneManager.GetActiveScene().name
|
||||||
|
!= SceneFlow.PlaymatSandbox)
|
||||||
|
{
|
||||||
_ = Flow.LoadLoginAsync();
|
_ = Flow.LoadLoginAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ee99f524fa5cd8849bef756b54365392
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 88e3b2aad35fabd47aa4f089166da98c
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
%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: 65b9427316955774f9c316282cfcda9a, type: 3}
|
||||||
|
m_Name: PlaymatLayoutConfig
|
||||||
|
m_EditorClassIdentifier: Vanguard.Runtime::Vanguard.Board3D.PlaymatLayoutConfig
|
||||||
|
cardWidth: 0.63
|
||||||
|
cardHeight: 0.88
|
||||||
|
cardThickness: 0.006
|
||||||
|
circleSpacingX: 1.42
|
||||||
|
frontRowZ: 1.55
|
||||||
|
backRowZ: 3
|
||||||
|
boardHalfWidth: 4.6
|
||||||
|
boardHalfLength: 4
|
||||||
|
cardLift: 0.02
|
||||||
|
guardianLift: 0.008
|
||||||
|
damageLift: 0.012
|
||||||
|
pileLift: 0.018
|
||||||
|
dragLift: 0.14
|
||||||
|
snapRadius: 0.66
|
||||||
|
dragThresholdPixels: 7
|
||||||
|
smoothMoveSpeed: 16
|
||||||
|
smoothRotateSpeed: 14
|
||||||
|
handHoverLift: 0.08
|
||||||
|
handCenterZ: -5.3
|
||||||
|
handHeight: 1.12
|
||||||
|
handTilt: 58
|
||||||
|
handSpacing: 0.42
|
||||||
|
handMaxWidth: 7.4
|
||||||
|
handArc: 0.035
|
||||||
|
guardianRowGap: 0.55
|
||||||
|
guardianSpacing: 0.44
|
||||||
|
guardianMaxWidth: 3.6
|
||||||
|
damageX: 3.5
|
||||||
|
pileX: 3.55
|
||||||
|
cameraPosition: {x: 0, y: 8.7, z: -12.4}
|
||||||
|
cameraTarget: {x: 0, y: 0, z: 0.3}
|
||||||
|
cameraFov: 50
|
||||||
|
orbitYawLimit: 32
|
||||||
|
orbitPitchMin: 20
|
||||||
|
orbitPitchMax: 62
|
||||||
|
zoomMin: 9.5
|
||||||
|
zoomMax: 16
|
||||||
|
zoomSpeed: 0.006
|
||||||
|
panSpeed: 0.0035
|
||||||
|
tableColor: {r: 0.045, g: 0.04, b: 0.055, a: 1}
|
||||||
|
matSelfColor: {r: 0.055, g: 0.095, b: 0.16, a: 1}
|
||||||
|
matOpponentColor: {r: 0.13, g: 0.065, b: 0.075, a: 1}
|
||||||
|
lineColor: {r: 0.62, g: 0.45, b: 0.18, a: 1}
|
||||||
|
ringColor: {r: 0.18, g: 0.28, b: 0.44, a: 1}
|
||||||
|
vanguardRingColor: {r: 0.78, g: 0.6, b: 0.24, a: 1}
|
||||||
|
guardianColor: {r: 0.18, g: 0.58, b: 0.7, a: 1}
|
||||||
|
damageColor: {r: 0.58, g: 0.16, b: 0.18, a: 1}
|
||||||
|
soulColor: {r: 0.42, g: 0.25, b: 0.68, a: 1}
|
||||||
|
highlightColor: {r: 1, g: 0.78, b: 0.24, a: 1}
|
||||||
|
selectedColor: {r: 0.35, g: 0.95, b: 0.56, a: 1}
|
||||||
|
cardPrefab: {fileID: 0}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c1f4dc13d22d0004ba1bddedecf71461
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Настройка 3D-поля: геометрия кругов, высоты, скорости анимаций,
|
||||||
|
/// камера и палитра. Один общий ассет на Classic; раскладка не зависит
|
||||||
|
/// от того, за какое место играет локальный игрок.
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(
|
||||||
|
menuName = "Vanguard/Playmat Layout",
|
||||||
|
fileName = "PlaymatLayoutConfig")]
|
||||||
|
public sealed class PlaymatLayoutConfig : ScriptableObject
|
||||||
|
{
|
||||||
|
[Header("Карта")]
|
||||||
|
public float cardWidth = 0.63f;
|
||||||
|
public float cardHeight = 0.88f;
|
||||||
|
public float cardThickness = 0.006f;
|
||||||
|
|
||||||
|
[Header("Поле")]
|
||||||
|
[Tooltip("Расстояние между центрами соседних кругов по X.")]
|
||||||
|
public float circleSpacingX = 1.42f;
|
||||||
|
[Tooltip("Z переднего ряда в локальных координатах игрока.")]
|
||||||
|
public float frontRowZ = 1.55f;
|
||||||
|
[Tooltip("Z заднего ряда в локальных координатах игрока.")]
|
||||||
|
public float backRowZ = 3.0f;
|
||||||
|
public float boardHalfWidth = 4.6f;
|
||||||
|
public float boardHalfLength = 4.0f;
|
||||||
|
|
||||||
|
[Header("Высоты")]
|
||||||
|
public float cardLift = 0.02f;
|
||||||
|
public float guardianLift = 0.008f;
|
||||||
|
public float damageLift = 0.012f;
|
||||||
|
public float pileLift = 0.018f;
|
||||||
|
public float dragLift = 0.14f;
|
||||||
|
|
||||||
|
[Header("Взаимодействие")]
|
||||||
|
public float snapRadius = 0.66f;
|
||||||
|
public float dragThresholdPixels = 7f;
|
||||||
|
public float smoothMoveSpeed = 16f;
|
||||||
|
public float smoothRotateSpeed = 14f;
|
||||||
|
public float handHoverLift = 0.08f;
|
||||||
|
|
||||||
|
[Header("Рука")]
|
||||||
|
public float handCenterZ = -5.3f;
|
||||||
|
public float handHeight = 1.12f;
|
||||||
|
[Range(0f, 85f)] public float handTilt = 58f;
|
||||||
|
public float handSpacing = 0.42f;
|
||||||
|
public float handMaxWidth = 7.4f;
|
||||||
|
public float handArc = 0.035f;
|
||||||
|
|
||||||
|
[Header("Круг защиты")]
|
||||||
|
public float guardianRowGap = 0.55f;
|
||||||
|
public float guardianSpacing = 0.44f;
|
||||||
|
public float guardianMaxWidth = 3.6f;
|
||||||
|
|
||||||
|
[Header("Стопки")]
|
||||||
|
[Tooltip("Расстояние от центра стола до зоны урона.")]
|
||||||
|
public float damageX = 3.5f;
|
||||||
|
[Tooltip("Расстояние от центра стола до колоды и дропа.")]
|
||||||
|
public float pileX = 3.55f;
|
||||||
|
|
||||||
|
[Header("Камера")]
|
||||||
|
public Vector3 cameraPosition = new Vector3(0f, 8.7f, -12.4f);
|
||||||
|
public Vector3 cameraTarget = new Vector3(0f, 0f, 0.3f);
|
||||||
|
public float cameraFov = 50f;
|
||||||
|
public float orbitYawLimit = 32f;
|
||||||
|
public float orbitPitchMin = 20f;
|
||||||
|
public float orbitPitchMax = 62f;
|
||||||
|
public float zoomMin = 9.5f;
|
||||||
|
public float zoomMax = 16f;
|
||||||
|
public float zoomSpeed = 0.006f;
|
||||||
|
public float panSpeed = 0.0035f;
|
||||||
|
|
||||||
|
[Header("Палитра")]
|
||||||
|
public Color tableColor = new Color(0.045f, 0.04f, 0.055f);
|
||||||
|
public Color matSelfColor = new Color(0.055f, 0.095f, 0.16f);
|
||||||
|
public Color matOpponentColor = new Color(0.13f, 0.065f, 0.075f);
|
||||||
|
public Color lineColor = new Color(0.62f, 0.45f, 0.18f);
|
||||||
|
public Color ringColor = new Color(0.18f, 0.28f, 0.44f);
|
||||||
|
public Color vanguardRingColor = new Color(0.78f, 0.60f, 0.24f);
|
||||||
|
public Color guardianColor = new Color(0.18f, 0.58f, 0.70f);
|
||||||
|
public Color damageColor = new Color(0.58f, 0.16f, 0.18f);
|
||||||
|
public Color soulColor = new Color(0.42f, 0.25f, 0.68f);
|
||||||
|
public Color highlightColor = new Color(1f, 0.78f, 0.24f);
|
||||||
|
public Color selectedColor = new Color(0.35f, 0.95f, 0.56f);
|
||||||
|
|
||||||
|
[Header("Необязательный prefab карты")]
|
||||||
|
public Card3D cardPrefab;
|
||||||
|
|
||||||
|
private static PlaymatLayoutConfig _default;
|
||||||
|
|
||||||
|
/// <summary>Рантайм-конфиг, если в сцене нет ассета.</summary>
|
||||||
|
public static PlaymatLayoutConfig Default
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_default == null)
|
||||||
|
{
|
||||||
|
_default = CreateInstance<PlaymatLayoutConfig>();
|
||||||
|
_default.name = "PlaymatLayoutConfig (runtime)";
|
||||||
|
_default.hideFlags = HideFlags.DontSave;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaymatLayoutConfig CreateDefault()
|
||||||
|
{
|
||||||
|
var config = CreateInstance<PlaymatLayoutConfig>();
|
||||||
|
config.name = "PlaymatLayoutConfig";
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float CardCenterY => cardLift + (cardThickness * 0.5f);
|
||||||
|
public float GuardianCenterY => guardianLift + (cardThickness * 0.5f);
|
||||||
|
public float DamageCenterY => damageLift + (cardThickness * 0.5f);
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
cardWidth = Mathf.Max(0.1f, cardWidth);
|
||||||
|
cardHeight = Mathf.Max(0.1f, cardHeight);
|
||||||
|
cardThickness = Mathf.Max(0.001f, cardThickness);
|
||||||
|
circleSpacingX = Mathf.Max(0.4f, circleSpacingX);
|
||||||
|
frontRowZ = Mathf.Max(0.2f, frontRowZ);
|
||||||
|
backRowZ = Mathf.Max(frontRowZ + 0.2f, backRowZ);
|
||||||
|
boardHalfWidth = Mathf.Max(2f, boardHalfWidth);
|
||||||
|
boardHalfLength = Mathf.Max(backRowZ + 0.5f, boardHalfLength);
|
||||||
|
dragLift = Mathf.Max(cardLift + 0.02f, dragLift);
|
||||||
|
snapRadius = Mathf.Clamp(snapRadius, 0.2f, 1.5f);
|
||||||
|
handMaxWidth = Mathf.Max(1f, handMaxWidth);
|
||||||
|
guardianMaxWidth = Mathf.Max(1f, guardianMaxWidth);
|
||||||
|
orbitPitchMax = Mathf.Clamp(orbitPitchMax, orbitPitchMin + 1f, 85f);
|
||||||
|
zoomMax = Mathf.Max(zoomMin + 0.5f, zoomMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 65b9427316955774f9c316282cfcda9a
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d0b844da79953df4bbe6319c79b3105a
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using Vanguard.Shared.Protocol;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>Круг поля. Classic использует V/R, остальные — задел на будущее.</summary>
|
||||||
|
public enum CircleAttribute
|
||||||
|
{
|
||||||
|
Vanguard,
|
||||||
|
RearGuard,
|
||||||
|
Guardian,
|
||||||
|
Damage,
|
||||||
|
Soul,
|
||||||
|
Deck,
|
||||||
|
Drop,
|
||||||
|
Hand,
|
||||||
|
Bind,
|
||||||
|
GenerationBreak,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Зона, к которой относится карта или пустой круг.</summary>
|
||||||
|
public enum BoardZone
|
||||||
|
{
|
||||||
|
Vanguard,
|
||||||
|
RearGuard,
|
||||||
|
Guardian,
|
||||||
|
Damage,
|
||||||
|
Soul,
|
||||||
|
Deck,
|
||||||
|
Drop,
|
||||||
|
Hand,
|
||||||
|
Bind,
|
||||||
|
GenerationBreak,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Положение карты на столе: открыта/закрыта и стоит/отдыхает.</summary>
|
||||||
|
public readonly struct CardState3D
|
||||||
|
{
|
||||||
|
public CardState3D(bool faceUp, bool rested)
|
||||||
|
{
|
||||||
|
FaceUp = faceUp;
|
||||||
|
Rested = rested;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FaceUp { get; }
|
||||||
|
public bool Rested { get; }
|
||||||
|
|
||||||
|
public static CardState3D FaceUpStand => new CardState3D(true, false);
|
||||||
|
public static CardState3D FaceDownStand => new CardState3D(false, false);
|
||||||
|
public static CardState3D FaceUpRest => new CardState3D(true, true);
|
||||||
|
public static CardState3D FaceDownRest => new CardState3D(false, true);
|
||||||
|
|
||||||
|
public override string ToString() =>
|
||||||
|
(FaceUp ? "up" : "down") + "/" + (Rested ? "rest" : "stand");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ключ круга для подсветки. Slot = -1 — Авангард.</summary>
|
||||||
|
public readonly struct BoardSlotKey : IEquatable<BoardSlotKey>
|
||||||
|
{
|
||||||
|
public BoardSlotKey(int player, int slot)
|
||||||
|
{
|
||||||
|
Player = player;
|
||||||
|
Slot = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Player { get; }
|
||||||
|
public int Slot { get; }
|
||||||
|
|
||||||
|
public bool Equals(BoardSlotKey other) =>
|
||||||
|
Player == other.Player && Slot == other.Slot;
|
||||||
|
|
||||||
|
public override bool Equals(object obj) =>
|
||||||
|
obj is BoardSlotKey other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode() => (Player * 31) + Slot;
|
||||||
|
|
||||||
|
public override string ToString() => Player + ":" + Slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ссылка на объект стола. Для поля заполнен Slot, для руки/урона/защиты —
|
||||||
|
/// Index. InstanceId = 0 у пустого круга и закрытой карты урона.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct BoardCardRef : IEquatable<BoardCardRef>
|
||||||
|
{
|
||||||
|
public BoardCardRef(
|
||||||
|
BoardZone zone,
|
||||||
|
int player,
|
||||||
|
int slot,
|
||||||
|
int index,
|
||||||
|
int instanceId,
|
||||||
|
string printingId,
|
||||||
|
bool faceUp)
|
||||||
|
{
|
||||||
|
Zone = zone;
|
||||||
|
Player = player;
|
||||||
|
Slot = slot;
|
||||||
|
Index = index;
|
||||||
|
InstanceId = instanceId;
|
||||||
|
PrintingId = printingId;
|
||||||
|
FaceUp = faceUp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BoardZone Zone { get; }
|
||||||
|
public int Player { get; }
|
||||||
|
public int Slot { get; }
|
||||||
|
public int Index { get; }
|
||||||
|
public int InstanceId { get; }
|
||||||
|
public string PrintingId { get; }
|
||||||
|
public bool FaceUp { get; }
|
||||||
|
|
||||||
|
public bool IsField => Zone is BoardZone.Vanguard or BoardZone.RearGuard;
|
||||||
|
public bool IsHand => Zone == BoardZone.Hand;
|
||||||
|
public bool IsEmpty => InstanceId == 0;
|
||||||
|
public BoardSlotKey SlotKey => new BoardSlotKey(Player, Slot);
|
||||||
|
|
||||||
|
public static BoardCardRef Field(int player, int slot, CardView card) =>
|
||||||
|
new BoardCardRef(
|
||||||
|
slot < 0 ? BoardZone.Vanguard : BoardZone.RearGuard,
|
||||||
|
player,
|
||||||
|
slot,
|
||||||
|
0,
|
||||||
|
card != null ? card.InstanceId : 0,
|
||||||
|
card != null ? card.PrintingId : null,
|
||||||
|
card != null && card.FaceUp);
|
||||||
|
|
||||||
|
public static BoardCardRef Hand(int player, int index, CardView card) =>
|
||||||
|
new BoardCardRef(
|
||||||
|
BoardZone.Hand,
|
||||||
|
player,
|
||||||
|
-2,
|
||||||
|
index,
|
||||||
|
card != null ? card.InstanceId : 0,
|
||||||
|
card != null ? card.PrintingId : null,
|
||||||
|
card != null && card.FaceUp);
|
||||||
|
|
||||||
|
public static BoardCardRef Guardian(int player, int index, CardView card) =>
|
||||||
|
new BoardCardRef(
|
||||||
|
BoardZone.Guardian,
|
||||||
|
player,
|
||||||
|
-3,
|
||||||
|
index,
|
||||||
|
card != null ? card.InstanceId : 0,
|
||||||
|
card != null ? card.PrintingId : null,
|
||||||
|
card != null && card.FaceUp);
|
||||||
|
|
||||||
|
public static BoardCardRef Damage(int player, int index, CardView card, bool faceUp) =>
|
||||||
|
new BoardCardRef(
|
||||||
|
BoardZone.Damage,
|
||||||
|
player,
|
||||||
|
-4,
|
||||||
|
index,
|
||||||
|
card != null ? card.InstanceId : 0,
|
||||||
|
card != null ? card.PrintingId : null,
|
||||||
|
faceUp);
|
||||||
|
|
||||||
|
public static BoardCardRef Pile(BoardZone zone, int player, int count) =>
|
||||||
|
new BoardCardRef(zone, player, -5, count, 0, null, false);
|
||||||
|
|
||||||
|
public static BoardCardRef EmptySlot(int player, int slot) =>
|
||||||
|
new BoardCardRef(
|
||||||
|
slot < 0 ? BoardZone.Vanguard : BoardZone.RearGuard,
|
||||||
|
player,
|
||||||
|
slot,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
public bool Equals(BoardCardRef other) =>
|
||||||
|
Zone == other.Zone
|
||||||
|
&& Player == other.Player
|
||||||
|
&& Slot == other.Slot
|
||||||
|
&& Index == other.Index
|
||||||
|
&& InstanceId == other.InstanceId;
|
||||||
|
|
||||||
|
public override bool Equals(object obj) =>
|
||||||
|
obj is BoardCardRef other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode() =>
|
||||||
|
(((int)Zone * 397) ^ Player * 31 ^ Slot) * 31 ^ Index ^ InstanceId;
|
||||||
|
|
||||||
|
public override string ToString() =>
|
||||||
|
Zone + ":" + Player + ":" + (IsField ? Slot.ToString() : Index.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Куда можно уронить карту при drag&drop.</summary>
|
||||||
|
public readonly struct BoardDropTarget
|
||||||
|
{
|
||||||
|
public BoardDropTarget(BoardZone zone, int player, int slot, int index)
|
||||||
|
{
|
||||||
|
Zone = zone;
|
||||||
|
Player = player;
|
||||||
|
Slot = slot;
|
||||||
|
Index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BoardZone Zone { get; }
|
||||||
|
public int Player { get; }
|
||||||
|
public int Slot { get; }
|
||||||
|
public int Index { get; }
|
||||||
|
|
||||||
|
public BoardCardRef ToRef() => new BoardCardRef(
|
||||||
|
Zone, Player, Slot, Index, 0, null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Временное состояние отрисовки, которое приходит из боевого экрана.</summary>
|
||||||
|
public sealed class BoardRenderState
|
||||||
|
{
|
||||||
|
public HashSet<BoardSlotKey> Highlights { get; } = new HashSet<BoardSlotKey>();
|
||||||
|
public HashSet<BoardSlotKey> DropTargets { get; } = new HashSet<BoardSlotKey>();
|
||||||
|
public bool GuardianDropAllowed { get; set; }
|
||||||
|
public HashSet<int> SelectedHandInstanceIds { get; } = new HashSet<int>();
|
||||||
|
public bool MulliganSelecting { get; set; }
|
||||||
|
public bool DamageChoice { get; set; }
|
||||||
|
public bool AbilityDamageTargeting { get; set; }
|
||||||
|
public int AbilityDamageIndex { get; set; } = -1;
|
||||||
|
public bool MatchFinished { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Текстура лица и акцентный цвет карты. Front может быть null.</summary>
|
||||||
|
public sealed class BoardArt
|
||||||
|
{
|
||||||
|
public BoardArt(Texture2D front, Color accent, string displayName = null)
|
||||||
|
{
|
||||||
|
Front = front;
|
||||||
|
Accent = accent;
|
||||||
|
DisplayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Texture2D Front { get; }
|
||||||
|
public Color Accent { get; }
|
||||||
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
public static BoardArt Fallback(Color accent) => new BoardArt(null, accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate BoardArt BoardArtProvider(string printingId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3d62441ca1c992648925ecfd2beafd34
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 94d93f21bd6f5bd4b94fda0ef65ea27d
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Локальные правила раскладки для UX. Сервер остаётся авторитарным:
|
||||||
|
/// эти проверки нужны для подсветки и drag&drop, а не для начисления
|
||||||
|
/// результата.
|
||||||
|
/// </summary>
|
||||||
|
public static class BoardPlacementRules
|
||||||
|
{
|
||||||
|
public static bool CanRide(BoardZone zone, int slot) =>
|
||||||
|
zone == BoardZone.Vanguard && slot == PlaymatLayout.VanguardSlot;
|
||||||
|
|
||||||
|
public static bool CanCall(int slot, bool occupied) =>
|
||||||
|
slot >= 0
|
||||||
|
&& slot < PlaymatLayout.RearGuardCount
|
||||||
|
&& !occupied;
|
||||||
|
|
||||||
|
public static bool CanGuard(BoardZone zone) =>
|
||||||
|
zone == BoardZone.Guardian;
|
||||||
|
|
||||||
|
public static bool CanMove(int fromSlot, int toSlot) =>
|
||||||
|
fromSlot >= 0
|
||||||
|
&& fromSlot < PlaymatLayout.RearGuardCount
|
||||||
|
&& toSlot >= 0
|
||||||
|
&& toSlot < PlaymatLayout.RearGuardCount
|
||||||
|
&& fromSlot != toSlot
|
||||||
|
&& SameColumn(fromSlot, toSlot);
|
||||||
|
|
||||||
|
public static bool CanAttack(int attackerSlot, int targetSlot) =>
|
||||||
|
attackerSlot >= PlaymatLayout.VanguardSlot
|
||||||
|
&& attackerSlot < PlaymatLayout.RearGuardCount
|
||||||
|
&& targetSlot >= PlaymatLayout.VanguardSlot
|
||||||
|
&& targetSlot < PlaymatLayout.RearGuardCount;
|
||||||
|
|
||||||
|
/// <summary>Колонки Classic: левая 0↔3, правая 2↔4; у центра пары нет.</summary>
|
||||||
|
public static bool SameColumn(int fromSlot, int toSlot) =>
|
||||||
|
(fromSlot, toSlot) is (0, 3) or (3, 0) or (2, 4) or (4, 2);
|
||||||
|
|
||||||
|
/// <summary>Call и Ride входят стоя и открытыми.</summary>
|
||||||
|
public static CardState3D EnterFromHand(CircleAttribute circle) => circle switch
|
||||||
|
{
|
||||||
|
CircleAttribute.Vanguard => CardState3D.FaceUpStand,
|
||||||
|
CircleAttribute.RearGuard => CardState3D.FaceUpStand,
|
||||||
|
CircleAttribute.Guardian => CardState3D.FaceUpStand,
|
||||||
|
_ => CardState3D.FaceUpStand,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Перемещение RC→RC сохраняет Rest и открытость.</summary>
|
||||||
|
public static CardState3D PreserveOnMove(CardState3D state) => state;
|
||||||
|
|
||||||
|
public static BoardZone ZoneOfSlot(int slot) =>
|
||||||
|
slot == PlaymatLayout.VanguardSlot
|
||||||
|
? BoardZone.Vanguard
|
||||||
|
: BoardZone.RearGuard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 511bb3e3238227e4eb651ed4d7d5d386
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Геометрия стола. Локальное место 0 — ближняя половина (локальный
|
||||||
|
/// игрок), 1 — дальняя (противник). Позиции всегда зеркальны, поэтому
|
||||||
|
/// игрок за второе место видит своё поле внизу.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlaymatLayout
|
||||||
|
{
|
||||||
|
public const int VanguardSlot = -1;
|
||||||
|
public const int RearGuardCount = 5;
|
||||||
|
public const int DamageSlots = 6;
|
||||||
|
|
||||||
|
private readonly PlaymatLayoutConfig _config;
|
||||||
|
|
||||||
|
public PlaymatLayout(PlaymatLayoutConfig config)
|
||||||
|
{
|
||||||
|
_config = config != null ? config : PlaymatLayoutConfig.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaymatLayoutConfig Config => _config;
|
||||||
|
|
||||||
|
public int ToLocalSeat(int player, int selfPlayer) =>
|
||||||
|
player == selfPlayer ? 0 : 1;
|
||||||
|
|
||||||
|
public int ToPlayer(int localSeat, int selfPlayer) =>
|
||||||
|
localSeat == 0 ? selfPlayer : 1 - selfPlayer;
|
||||||
|
|
||||||
|
/// <summary>Z-направление от центра к половине локального места.</summary>
|
||||||
|
public float ZSign(int localSeat) => localSeat == 0 ? -1f : 1f;
|
||||||
|
|
||||||
|
/// <summary>Поворот лица карты: своя половина читается с -Z, чужая — с +Z.</summary>
|
||||||
|
public float BaseYaw(int localSeat) => localSeat == 0 ? 0f : 180f;
|
||||||
|
|
||||||
|
public Vector3 CirclePosition(int localSeat, int slot)
|
||||||
|
{
|
||||||
|
var zSign = ZSign(localSeat);
|
||||||
|
var columnSign = localSeat == 0 ? 1f : -1f;
|
||||||
|
var x = 0f;
|
||||||
|
var z = slot == VanguardSlot || slot is 3 or 4
|
||||||
|
? zSign * _config.frontRowZ
|
||||||
|
: zSign * _config.backRowZ;
|
||||||
|
|
||||||
|
switch (slot)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
case 3:
|
||||||
|
x = -_config.circleSpacingX * columnSign;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
case 4:
|
||||||
|
x = _config.circleSpacingX * columnSign;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Vector3(x, _config.CardCenterY, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 CirclePositionForPlayer(int player, int selfPlayer, int slot) =>
|
||||||
|
CirclePosition(ToLocalSeat(player, selfPlayer), slot);
|
||||||
|
|
||||||
|
/// <summary>Круги урона: две колонки по три карты на боку.</summary>
|
||||||
|
public Vector3 DamagePosition(int localSeat, int index)
|
||||||
|
{
|
||||||
|
var zSign = ZSign(localSeat);
|
||||||
|
var sideSign = localSeat == 0 ? -1f : 1f;
|
||||||
|
var innerSign = localSeat == 0 ? -1f : 1f;
|
||||||
|
var xCenter = sideSign * _config.damageX;
|
||||||
|
var x = xCenter + (index % 2 == 0 ? innerSign : -innerSign) * 0.42f;
|
||||||
|
var z = zSign * (_config.frontRowZ + 0.45f)
|
||||||
|
+ zSign * (index / 2) * 0.55f;
|
||||||
|
return new Vector3(x, _config.DamageCenterY, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Колода и дроп сбоку от поля, у своего края.</summary>
|
||||||
|
public Vector3 DeckPosition(int localSeat)
|
||||||
|
{
|
||||||
|
var x = (localSeat == 0 ? 1f : -1f) * _config.pileX;
|
||||||
|
var z = ZSign(localSeat) * (_config.backRowZ + 0.15f);
|
||||||
|
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);
|
||||||
|
return new Vector3(x, _config.pileLift, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Душа — визуальная стопка под кругом V, чуть ближе к игроку.</summary>
|
||||||
|
public Vector3 SoulPosition(int localSeat)
|
||||||
|
{
|
||||||
|
var z = ZSign(localSeat) * (_config.frontRowZ + 0.55f);
|
||||||
|
return new Vector3(0f, 0.002f, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Общий круг защиты: своя половина ближе к своему полю.</summary>
|
||||||
|
public Vector3 GuardianPosition(int localSeat, int index, int count)
|
||||||
|
{
|
||||||
|
var spacing = _config.guardianSpacing;
|
||||||
|
if (count > 1)
|
||||||
|
spacing = Mathf.Min(spacing, _config.guardianMaxWidth / (count - 1));
|
||||||
|
|
||||||
|
var offset = index - (count - 1) * 0.5f;
|
||||||
|
var z = ZSign(localSeat) * _config.guardianRowGap;
|
||||||
|
return new Vector3(offset * spacing, _config.GuardianCenterY, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Веер руки перед локальным игроком.</summary>
|
||||||
|
public HandPose GetHandPose(int index, int count)
|
||||||
|
{
|
||||||
|
var safeCount = Mathf.Max(1, count);
|
||||||
|
var spacing = _config.handSpacing;
|
||||||
|
if (safeCount > 1)
|
||||||
|
spacing = Mathf.Min(spacing, _config.handMaxWidth / (safeCount - 1));
|
||||||
|
|
||||||
|
var offset = index - (safeCount - 1) * 0.5f;
|
||||||
|
var normalized = offset / safeCount;
|
||||||
|
var x = offset * spacing;
|
||||||
|
var z = _config.handCenterZ + Mathf.Abs(offset) * _config.handArc;
|
||||||
|
var y = _config.handHeight + (1f - Mathf.Abs(normalized)) * 0.02f;
|
||||||
|
var yaw = -offset * 3.2f;
|
||||||
|
return new HandPose(new Vector3(x, y, z), yaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ближайшая цель для броска карты. Возвращает false, если карта
|
||||||
|
/// отпущена мимо кругов и общего круга защиты.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryFindDropTarget(
|
||||||
|
Vector3 worldPoint,
|
||||||
|
int selfPlayer,
|
||||||
|
out BoardDropTarget target)
|
||||||
|
{
|
||||||
|
var bestDistance = float.MaxValue;
|
||||||
|
target = default;
|
||||||
|
|
||||||
|
for (var localSeat = 0; localSeat < 2; localSeat++)
|
||||||
|
{
|
||||||
|
for (var slot = VanguardSlot; slot < RearGuardCount; slot++)
|
||||||
|
{
|
||||||
|
var position = CirclePosition(localSeat, slot);
|
||||||
|
var distance = XzDistance(worldPoint, position);
|
||||||
|
if (distance > _config.snapRadius || distance >= bestDistance)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bestDistance = distance;
|
||||||
|
target = new BoardDropTarget(
|
||||||
|
slot == VanguardSlot ? BoardZone.Vanguard : BoardZone.RearGuard,
|
||||||
|
ToPlayer(localSeat, selfPlayer),
|
||||||
|
slot,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestDistance < float.MaxValue)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Общий круг защиты принимает карты с любой стороны.
|
||||||
|
if (Mathf.Abs(worldPoint.x) <= _config.guardianMaxWidth * 0.5f + 0.35f
|
||||||
|
&& Mathf.Abs(worldPoint.z) <= _config.guardianRowGap + 0.45f)
|
||||||
|
{
|
||||||
|
var localSeat = worldPoint.z < 0f ? 0 : 1;
|
||||||
|
target = new BoardDropTarget(
|
||||||
|
BoardZone.Guardian,
|
||||||
|
ToPlayer(localSeat, selfPlayer),
|
||||||
|
-3,
|
||||||
|
0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float XzDistance(Vector3 a, Vector3 b)
|
||||||
|
{
|
||||||
|
var dx = a.x - b.x;
|
||||||
|
var dz = a.z - b.z;
|
||||||
|
return Mathf.Sqrt((dx * dx) + (dz * dz));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsFrontRow(int slot) => slot is 3 or 4;
|
||||||
|
public static bool IsBackRow(int slot) => slot is 0 or 1 or 2;
|
||||||
|
|
||||||
|
public static string SlotLabel(int slot)
|
||||||
|
{
|
||||||
|
if (slot == VanguardSlot)
|
||||||
|
return "V";
|
||||||
|
|
||||||
|
return slot switch
|
||||||
|
{
|
||||||
|
0 => "RC1",
|
||||||
|
1 => "RC2",
|
||||||
|
2 => "RC3",
|
||||||
|
3 => "RC4",
|
||||||
|
4 => "RC5",
|
||||||
|
_ => "?",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct HandPose
|
||||||
|
{
|
||||||
|
public HandPose(Vector3 position, float yaw)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
Yaw = yaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 Position { get; }
|
||||||
|
public float Yaw { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ccfc6e38ed67c204f9b17de5edccbf6a
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using Vanguard.App;
|
||||||
|
using Vanguard.Shared.Protocol;
|
||||||
|
using Vanguard.UI;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Демо-сцена 3D-поля: раскладывает карты без сервера, чтобы проверять
|
||||||
|
/// геометрию, drag&drop, rest, закрытые карты и общий круг защиты.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlaymatSandbox : MonoBehaviour
|
||||||
|
{
|
||||||
|
public PlaymatLayoutConfig config;
|
||||||
|
public bool showHints = true;
|
||||||
|
|
||||||
|
private GameBoard3D _board;
|
||||||
|
private int _clicks;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
SceneEnvironment.Build(SceneTheme.Battle);
|
||||||
|
|
||||||
|
_board = GameBoard3D.Create(null, config != null ? config : PlaymatLayoutConfig.Default);
|
||||||
|
_board.ArtProvider = SandboxArt;
|
||||||
|
_board.SetVisible(true);
|
||||||
|
_board.CardClicked += OnCardClicked;
|
||||||
|
_board.SlotClicked += OnSlotClicked;
|
||||||
|
_board.CardDropped += OnCardDropped;
|
||||||
|
_board.PileClicked += OnPileClicked;
|
||||||
|
_board.Render(BuildDemo(), BuildState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCardClicked(BoardCardRef card)
|
||||||
|
{
|
||||||
|
_clicks++;
|
||||||
|
Debug.Log("SANDBOX card " + card);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSlotClicked(BoardCardRef slot)
|
||||||
|
{
|
||||||
|
Debug.Log("SANDBOX slot " + slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCardDropped(BoardCardRef source, BoardCardRef target)
|
||||||
|
{
|
||||||
|
Debug.Log("SANDBOX drop " + source + " -> " + target);
|
||||||
|
// В песочнице нет сервера: возвращаем карту на место.
|
||||||
|
_board.Render(BuildDemo(), BuildState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPileClicked(BoardCardRef pile)
|
||||||
|
{
|
||||||
|
Debug.Log("SANDBOX pile " + pile + " count=" + pile.Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoardArt SandboxArt(string printingId)
|
||||||
|
{
|
||||||
|
var clan = printingId != null && printingId.Contains("002")
|
||||||
|
? "Kagero"
|
||||||
|
: "RoyalPaladin";
|
||||||
|
var sprite = CardArt.ForCard(printingId ?? "sandbox", clan);
|
||||||
|
return new BoardArt(
|
||||||
|
sprite != null ? sprite.texture : null,
|
||||||
|
CardArt.ClanColor(clan),
|
||||||
|
printingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoardRenderState BuildState()
|
||||||
|
{
|
||||||
|
var state = new BoardRenderState();
|
||||||
|
state.Highlights.Add(new BoardSlotKey(0, -1));
|
||||||
|
state.Highlights.Add(new BoardSlotKey(1, -1));
|
||||||
|
state.SelectedHandInstanceIds.Add(12);
|
||||||
|
state.DropTargets.Add(new BoardSlotKey(0, -1));
|
||||||
|
for (var slot = 0; slot < PlaymatLayout.RearGuardCount; slot++)
|
||||||
|
state.DropTargets.Add(new BoardSlotKey(0, slot));
|
||||||
|
state.GuardianDropAllowed = true;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MatchView BuildDemo()
|
||||||
|
{
|
||||||
|
CardView Card(string id, int instance, bool rested = false, bool faceUp = true) =>
|
||||||
|
new CardView
|
||||||
|
{
|
||||||
|
PrintingId = id,
|
||||||
|
InstanceId = instance,
|
||||||
|
Rested = rested,
|
||||||
|
FaceUp = faceUp,
|
||||||
|
BasePower = 8000,
|
||||||
|
Power = rested ? 5000 : 8000,
|
||||||
|
BaseCritical = 1,
|
||||||
|
Critical = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
var self = new PlayerView
|
||||||
|
{
|
||||||
|
Index = 0,
|
||||||
|
Name = "Sandbox",
|
||||||
|
Vanguard = Card("BT01/001EN", 1),
|
||||||
|
RearGuards = new CardView[5],
|
||||||
|
Hand = new List<CardView>
|
||||||
|
{
|
||||||
|
Card("BT01/002EN", 11),
|
||||||
|
Card("BT01/003EN", 12),
|
||||||
|
Card("BT01/004EN", 13),
|
||||||
|
Card("BT01/005EN", 14),
|
||||||
|
Card("BT01/006EN", 15),
|
||||||
|
},
|
||||||
|
Guardian = new List<CardView>
|
||||||
|
{
|
||||||
|
Card("BT01/007EN", 21),
|
||||||
|
Card("BT01/008EN", 22),
|
||||||
|
},
|
||||||
|
Soul = new List<CardView>
|
||||||
|
{
|
||||||
|
Card("BT01/009EN", 31),
|
||||||
|
Card("BT01/010EN", 32),
|
||||||
|
},
|
||||||
|
FaceDownDamage = 2,
|
||||||
|
FaceUpDamage = new List<CardView>
|
||||||
|
{
|
||||||
|
Card("BT01/011EN", 41),
|
||||||
|
Card("BT01/012EN", 42),
|
||||||
|
},
|
||||||
|
DeckCount = 38,
|
||||||
|
Drop = new List<CardView> { Card("BT01/013EN", 51) },
|
||||||
|
};
|
||||||
|
self.RearGuards[0] = Card("BT01/014EN", 2);
|
||||||
|
self.RearGuards[3] = Card("BT01/015EN", 3, rested: true);
|
||||||
|
self.RearGuards[4] = Card("BT01/016EN", 4);
|
||||||
|
|
||||||
|
var opponent = new PlayerView
|
||||||
|
{
|
||||||
|
Index = 1,
|
||||||
|
Name = "Opponent",
|
||||||
|
Vanguard = Card("BT01/020EN", 101),
|
||||||
|
RearGuards = new CardView[5],
|
||||||
|
Hand = new List<CardView>(),
|
||||||
|
Guardian = new List<CardView>(),
|
||||||
|
Soul = new List<CardView> { Card("BT01/021EN", 131) },
|
||||||
|
FaceDownDamage = 1,
|
||||||
|
FaceUpDamage = new List<CardView> { Card("BT01/022EN", 141) },
|
||||||
|
DeckCount = 40,
|
||||||
|
Drop = new List<CardView>(),
|
||||||
|
};
|
||||||
|
opponent.RearGuards[4] = Card("BT01/023EN", 102);
|
||||||
|
opponent.RearGuards[2] = Card("BT01/024EN", 103, rested: true);
|
||||||
|
|
||||||
|
return new MatchView
|
||||||
|
{
|
||||||
|
MatchId = "sandbox",
|
||||||
|
You = 0,
|
||||||
|
ActivePlayer = 0,
|
||||||
|
TurnNumber = 4,
|
||||||
|
Phase = GamePhase.Main,
|
||||||
|
Players = new List<PlayerView> { self, opponent },
|
||||||
|
Clock = new ClockView(),
|
||||||
|
LegalIntents = new List<IntentPayload>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 89fedfc3c1f371c448b4187e210d36fb
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1db33b73c2d4ad645b7ad7a7e6aaff6b
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.EventSystems;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ограниченная орбита камеры: ПКМ — поворот, СКМ — панорама,
|
||||||
|
/// колесо — зум. Левая кнопка остаётся картам и UI.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BoardCameraRig : MonoBehaviour
|
||||||
|
{
|
||||||
|
private PlaymatLayoutConfig _config;
|
||||||
|
private Vector3 _target;
|
||||||
|
private float _distance = 15f;
|
||||||
|
private float _yaw;
|
||||||
|
private float _pitch = 35f;
|
||||||
|
private bool _orbiting;
|
||||||
|
private bool _panning;
|
||||||
|
private Vector2 _lastPointer;
|
||||||
|
|
||||||
|
public void Configure(PlaymatLayoutConfig config)
|
||||||
|
{
|
||||||
|
_config = config != null ? config : PlaymatLayoutConfig.Default;
|
||||||
|
_target = _config.cameraTarget;
|
||||||
|
var offset = _config.cameraPosition - _target;
|
||||||
|
_distance = Mathf.Clamp(
|
||||||
|
offset.magnitude,
|
||||||
|
_config.zoomMin,
|
||||||
|
_config.zoomMax);
|
||||||
|
_pitch = Mathf.Clamp(
|
||||||
|
Mathf.Asin(Mathf.Clamp(offset.y / Mathf.Max(0.001f, _distance), -1f, 1f))
|
||||||
|
* Mathf.Rad2Deg,
|
||||||
|
_config.orbitPitchMin,
|
||||||
|
_config.orbitPitchMax);
|
||||||
|
_yaw = Mathf.Atan2(-offset.x, -offset.z) * Mathf.Rad2Deg;
|
||||||
|
_yaw = Mathf.Clamp(_yaw, -_config.orbitYawLimit, _config.orbitYawLimit);
|
||||||
|
Apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (_config == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var mouse = Mouse.current;
|
||||||
|
if (mouse == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pointer = mouse.position.ReadValue();
|
||||||
|
var overUi = IsPointerOverUi();
|
||||||
|
|
||||||
|
if (mouse.rightButton.wasPressedThisFrame && !overUi)
|
||||||
|
{
|
||||||
|
_orbiting = true;
|
||||||
|
_lastPointer = pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse.rightButton.wasReleasedThisFrame)
|
||||||
|
_orbiting = false;
|
||||||
|
|
||||||
|
if (_orbiting && mouse.rightButton.isPressed)
|
||||||
|
{
|
||||||
|
var delta = pointer - _lastPointer;
|
||||||
|
_lastPointer = pointer;
|
||||||
|
_yaw = Mathf.Clamp(
|
||||||
|
_yaw + (delta.x * 0.18f),
|
||||||
|
-_config.orbitYawLimit,
|
||||||
|
_config.orbitYawLimit);
|
||||||
|
_pitch = Mathf.Clamp(
|
||||||
|
_pitch - (delta.y * 0.16f),
|
||||||
|
_config.orbitPitchMin,
|
||||||
|
_config.orbitPitchMax);
|
||||||
|
Apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse.middleButton.wasPressedThisFrame && !overUi)
|
||||||
|
{
|
||||||
|
_panning = true;
|
||||||
|
_lastPointer = pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse.middleButton.wasReleasedThisFrame)
|
||||||
|
_panning = false;
|
||||||
|
|
||||||
|
if (_panning && mouse.middleButton.isPressed)
|
||||||
|
{
|
||||||
|
var delta = pointer - _lastPointer;
|
||||||
|
_lastPointer = pointer;
|
||||||
|
var rotation = Quaternion.Euler(_pitch, _yaw, 0f);
|
||||||
|
var right = rotation * Vector3.right;
|
||||||
|
var forward = rotation * Vector3.forward;
|
||||||
|
right.y = 0f;
|
||||||
|
forward.y = 0f;
|
||||||
|
right.Normalize();
|
||||||
|
forward.Normalize();
|
||||||
|
|
||||||
|
var scale = _config.panSpeed * _distance;
|
||||||
|
_target -= (right * (delta.x * scale)) + (forward * (delta.y * scale));
|
||||||
|
_target.x = Mathf.Clamp(_target.x, -2.5f, 2.5f);
|
||||||
|
_target.z = Mathf.Clamp(_target.z, -2f, 2f);
|
||||||
|
Apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
var scroll = mouse.scroll.ReadValue().y;
|
||||||
|
if (!overUi && Mathf.Abs(scroll) > 0.01f)
|
||||||
|
{
|
||||||
|
_distance = Mathf.Clamp(
|
||||||
|
_distance - (scroll * _config.zoomSpeed),
|
||||||
|
_config.zoomMin,
|
||||||
|
_config.zoomMax);
|
||||||
|
Apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Apply()
|
||||||
|
{
|
||||||
|
var rotation = Quaternion.Euler(_pitch, _yaw, 0f);
|
||||||
|
transform.rotation = rotation;
|
||||||
|
transform.position = _target - (rotation * Vector3.forward * _distance);
|
||||||
|
var camera = GetComponent<Camera>();
|
||||||
|
if (camera != null)
|
||||||
|
camera.fieldOfView = _config.cameraFov;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPointerOverUi()
|
||||||
|
{
|
||||||
|
var eventSystem = EventSystem.current;
|
||||||
|
return eventSystem != null && eventSystem.IsPointerOverGameObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 30bd2e29295c0b241bb1f29e9dc73ca8
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Rendering;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>Общие материалы стола. Текстуры лиц задаются property block.</summary>
|
||||||
|
internal static class BoardMaterials
|
||||||
|
{
|
||||||
|
private static Material _edge;
|
||||||
|
private static Material _back;
|
||||||
|
private static Material _highlight;
|
||||||
|
private static Material _white;
|
||||||
|
|
||||||
|
public static Material Edge
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_edge == null)
|
||||||
|
_edge = CreateLit(new Color(0.10f, 0.11f, 0.14f));
|
||||||
|
return _edge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Material Back
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_back == null)
|
||||||
|
_back = CreateUnlit(BoardTextures.Back, Color.white);
|
||||||
|
return _back;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Material Highlight
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_highlight == null)
|
||||||
|
_highlight = CreateUnlit(null, new Color(1f, 0.78f, 0.24f));
|
||||||
|
return _highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Material White
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_white == null)
|
||||||
|
_white = CreateUnlit(null, Color.white);
|
||||||
|
return _white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Material CreateLit(Color color, float emission = 0f)
|
||||||
|
{
|
||||||
|
var shader = Shader.Find("Universal Render Pipeline/Lit");
|
||||||
|
if (shader == null)
|
||||||
|
shader = Shader.Find("Standard");
|
||||||
|
if (shader == null)
|
||||||
|
return CreateUnlit(null, color);
|
||||||
|
|
||||||
|
var material = new Material(shader)
|
||||||
|
{
|
||||||
|
name = "Board Lit",
|
||||||
|
enableInstancing = true,
|
||||||
|
hideFlags = HideFlags.DontSave,
|
||||||
|
};
|
||||||
|
SetColor(material, color);
|
||||||
|
if (emission > 0f && material.HasProperty("_EmissionColor"))
|
||||||
|
{
|
||||||
|
material.EnableKeyword("_EMISSION");
|
||||||
|
material.SetColor("_EmissionColor", color * emission);
|
||||||
|
}
|
||||||
|
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Material CreateUnlit(Texture texture, Color tint)
|
||||||
|
{
|
||||||
|
var shader = Shader.Find("Universal Render Pipeline/Unlit");
|
||||||
|
if (shader == null)
|
||||||
|
shader = Shader.Find("Unlit/Texture");
|
||||||
|
if (shader == null)
|
||||||
|
shader = Shader.Find("Sprites/Default");
|
||||||
|
if (shader == null)
|
||||||
|
shader = Shader.Find("Standard");
|
||||||
|
|
||||||
|
var material = new Material(shader)
|
||||||
|
{
|
||||||
|
name = "Board Unlit",
|
||||||
|
enableInstancing = true,
|
||||||
|
hideFlags = HideFlags.DontSave,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (texture != null)
|
||||||
|
{
|
||||||
|
if (material.HasProperty("_BaseMap"))
|
||||||
|
material.SetTexture("_BaseMap", texture);
|
||||||
|
if (material.HasProperty("_MainTex"))
|
||||||
|
material.SetTexture("_MainTex", texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetColor(material, tint);
|
||||||
|
if (material.HasProperty("_Surface"))
|
||||||
|
material.SetFloat("_Surface", 0f);
|
||||||
|
if (material.HasProperty("_Blend"))
|
||||||
|
material.SetFloat("_Blend", 0f);
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetColor(Material material, Color color)
|
||||||
|
{
|
||||||
|
if (material == null)
|
||||||
|
return;
|
||||||
|
if (material.HasProperty("_BaseColor"))
|
||||||
|
material.SetColor("_BaseColor", color);
|
||||||
|
if (material.HasProperty("_Color"))
|
||||||
|
material.SetColor("_Color", color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetColor(MaterialPropertyBlock block, Material material, Color color)
|
||||||
|
{
|
||||||
|
if (block == null)
|
||||||
|
return;
|
||||||
|
if (material != null && material.HasProperty("_BaseColor"))
|
||||||
|
block.SetColor("_BaseColor", color);
|
||||||
|
else
|
||||||
|
block.SetColor("_Color", color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetTexture(MaterialPropertyBlock block, Material material, Texture texture)
|
||||||
|
{
|
||||||
|
if (block == null)
|
||||||
|
return;
|
||||||
|
if (material != null && material.HasProperty("_BaseMap"))
|
||||||
|
block.SetTexture("_BaseMap", texture);
|
||||||
|
else
|
||||||
|
block.SetTexture("_MainTex", texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetShadows(Renderer renderer, bool cast)
|
||||||
|
{
|
||||||
|
if (renderer == null)
|
||||||
|
return;
|
||||||
|
renderer.shadowCastingMode = cast
|
||||||
|
? ShadowCastingMode.On
|
||||||
|
: ShadowCastingMode.Off;
|
||||||
|
renderer.receiveShadows = cast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ee026e3b2fff0b548b3a06f4c6daab77
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>Плоские рамки кругов и зон в плоскости XZ.</summary>
|
||||||
|
internal static class BoardMeshLibrary
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, Mesh> Cache =
|
||||||
|
new Dictionary<string, Mesh>();
|
||||||
|
|
||||||
|
public static Mesh Frame(float width, float depth, float border)
|
||||||
|
{
|
||||||
|
var key = Mathf.RoundToInt(width * 1000f)
|
||||||
|
+ ":" + Mathf.RoundToInt(depth * 1000f)
|
||||||
|
+ ":" + Mathf.RoundToInt(border * 1000f);
|
||||||
|
if (Cache.TryGetValue(key, out var cached) && cached != null)
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var mesh = BuildFrame(width, depth, border);
|
||||||
|
mesh.hideFlags = HideFlags.DontSave;
|
||||||
|
Cache[key] = mesh;
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Mesh CardFace(float width, float height, bool front)
|
||||||
|
{
|
||||||
|
var key = (front ? "front:" : "back:")
|
||||||
|
+ Mathf.RoundToInt(width * 1000f)
|
||||||
|
+ ":" + Mathf.RoundToInt(height * 1000f);
|
||||||
|
if (Cache.TryGetValue(key, out var cached) && cached != null)
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var halfW = width * 0.5f;
|
||||||
|
var halfH = height * 0.5f;
|
||||||
|
var normal = front ? Vector3.up : Vector3.down;
|
||||||
|
var mesh = new Mesh
|
||||||
|
{
|
||||||
|
name = front ? "Card Front" : "Card Back",
|
||||||
|
hideFlags = HideFlags.DontSave,
|
||||||
|
vertices = new[]
|
||||||
|
{
|
||||||
|
new Vector3(-halfW, 0f, -halfH),
|
||||||
|
new Vector3(halfW, 0f, -halfH),
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
normals = new[] { normal, normal, normal, normal },
|
||||||
|
triangles = new[]
|
||||||
|
{
|
||||||
|
0, 1, 2, 0, 2, 3,
|
||||||
|
0, 2, 1, 0, 3, 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mesh.RecalculateBounds();
|
||||||
|
Cache[key] = mesh;
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mesh BuildFrame(float width, float depth, float border)
|
||||||
|
{
|
||||||
|
var halfW = width * 0.5f;
|
||||||
|
var halfD = depth * 0.5f;
|
||||||
|
border = Mathf.Clamp(border, 0.005f, Mathf.Min(halfW, halfD) * 0.9f);
|
||||||
|
|
||||||
|
var vertices = new List<Vector3>(16);
|
||||||
|
var triangles = new List<int>(48);
|
||||||
|
var uvs = new List<Vector2>(16);
|
||||||
|
|
||||||
|
AddQuad(
|
||||||
|
vertices, triangles, uvs,
|
||||||
|
new Vector3(-halfW, 0f, halfD - border),
|
||||||
|
new Vector3(halfW, 0f, halfD - border),
|
||||||
|
new Vector3(halfW, 0f, halfD),
|
||||||
|
new Vector3(-halfW, 0f, halfD));
|
||||||
|
|
||||||
|
AddQuad(
|
||||||
|
vertices, triangles, uvs,
|
||||||
|
new Vector3(-halfW, 0f, -halfD),
|
||||||
|
new Vector3(halfW, 0f, -halfD),
|
||||||
|
new Vector3(halfW, 0f, -halfD + border),
|
||||||
|
new Vector3(-halfW, 0f, -halfD + border));
|
||||||
|
|
||||||
|
AddQuad(
|
||||||
|
vertices, triangles, uvs,
|
||||||
|
new Vector3(-halfW, 0f, -halfD + border),
|
||||||
|
new Vector3(-halfW + border, 0f, -halfD + border),
|
||||||
|
new Vector3(-halfW + border, 0f, halfD - border),
|
||||||
|
new Vector3(-halfW, 0f, halfD - border));
|
||||||
|
|
||||||
|
AddQuad(
|
||||||
|
vertices, triangles, uvs,
|
||||||
|
new Vector3(halfW - border, 0f, -halfD + border),
|
||||||
|
new Vector3(halfW, 0f, -halfD + border),
|
||||||
|
new Vector3(halfW, 0f, halfD - border),
|
||||||
|
new Vector3(halfW - border, 0f, halfD - border));
|
||||||
|
|
||||||
|
var mesh = new Mesh
|
||||||
|
{
|
||||||
|
name = "Board Frame",
|
||||||
|
vertices = vertices.ToArray(),
|
||||||
|
triangles = triangles.ToArray(),
|
||||||
|
uv = uvs.ToArray(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var normals = new Vector3[vertices.Count];
|
||||||
|
for (var i = 0; i < normals.Length; i++)
|
||||||
|
normals[i] = Vector3.up;
|
||||||
|
mesh.normals = normals;
|
||||||
|
mesh.RecalculateBounds();
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddQuad(
|
||||||
|
ICollection<Vector3> vertices,
|
||||||
|
ICollection<int> triangles,
|
||||||
|
ICollection<Vector2> uvs,
|
||||||
|
Vector3 a,
|
||||||
|
Vector3 b,
|
||||||
|
Vector3 c,
|
||||||
|
Vector3 d)
|
||||||
|
{
|
||||||
|
var start = vertices.Count;
|
||||||
|
vertices.Add(a);
|
||||||
|
vertices.Add(b);
|
||||||
|
vertices.Add(c);
|
||||||
|
vertices.Add(d);
|
||||||
|
|
||||||
|
uvs.Add(new Vector2(0f, 0f));
|
||||||
|
uvs.Add(new Vector2(1f, 0f));
|
||||||
|
uvs.Add(new Vector2(1f, 1f));
|
||||||
|
uvs.Add(new Vector2(0f, 1f));
|
||||||
|
|
||||||
|
// Две стороны: рамки видны и сверху, и при наклоне камеры.
|
||||||
|
triangles.Add(start);
|
||||||
|
triangles.Add(start + 1);
|
||||||
|
triangles.Add(start + 2);
|
||||||
|
triangles.Add(start);
|
||||||
|
triangles.Add(start + 2);
|
||||||
|
triangles.Add(start + 3);
|
||||||
|
triangles.Add(start);
|
||||||
|
triangles.Add(start + 2);
|
||||||
|
triangles.Add(start + 1);
|
||||||
|
triangles.Add(start);
|
||||||
|
triangles.Add(start + 3);
|
||||||
|
triangles.Add(start + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b08a1484f6888564c83043946c9467b6
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>Круг поля: рамка, коллайдер и подпись V/R.</summary>
|
||||||
|
public sealed class BoardSlotView : MonoBehaviour
|
||||||
|
{
|
||||||
|
private MeshRenderer _renderer;
|
||||||
|
private MaterialPropertyBlock _block;
|
||||||
|
private Color _baseColor;
|
||||||
|
private Color _highlightColor;
|
||||||
|
|
||||||
|
public int Player { get; private set; }
|
||||||
|
public int Slot { get; private set; }
|
||||||
|
public int LocalSeat { get; private set; }
|
||||||
|
public BoardSlotKey Key => new BoardSlotKey(Player, Slot);
|
||||||
|
public BoardCardRef Ref => BoardCardRef.EmptySlot(Player, Slot);
|
||||||
|
|
||||||
|
public void SetPlayer(int player)
|
||||||
|
{
|
||||||
|
Player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BoardSlotView Create(
|
||||||
|
Transform parent,
|
||||||
|
PlaymatLayoutConfig config,
|
||||||
|
int localSeat,
|
||||||
|
int player,
|
||||||
|
int slot,
|
||||||
|
PlaymatLayout layout)
|
||||||
|
{
|
||||||
|
var go = new GameObject("Slot " + PlaymatLayout.SlotLabel(slot));
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
go.transform.localPosition = layout.CirclePosition(localSeat, slot);
|
||||||
|
go.transform.localPosition = new Vector3(
|
||||||
|
go.transform.localPosition.x,
|
||||||
|
0.012f,
|
||||||
|
go.transform.localPosition.z);
|
||||||
|
|
||||||
|
var view = go.AddComponent<BoardSlotView>();
|
||||||
|
view.Player = player;
|
||||||
|
view.Slot = slot;
|
||||||
|
view.LocalSeat = localSeat;
|
||||||
|
view._baseColor = slot == PlaymatLayout.VanguardSlot
|
||||||
|
? config.vanguardRingColor
|
||||||
|
: config.ringColor;
|
||||||
|
view._highlightColor = config.highlightColor;
|
||||||
|
view._block = new MaterialPropertyBlock();
|
||||||
|
|
||||||
|
var filter = go.AddComponent<MeshFilter>();
|
||||||
|
filter.sharedMesh = BoardMeshLibrary.Frame(
|
||||||
|
config.cardWidth * 1.34f,
|
||||||
|
config.cardHeight * 1.34f,
|
||||||
|
Mathf.Max(0.018f, config.cardWidth * 0.035f));
|
||||||
|
|
||||||
|
var renderer = go.AddComponent<MeshRenderer>();
|
||||||
|
renderer.sharedMaterial = BoardMaterials.Highlight;
|
||||||
|
BoardMaterials.SetShadows(renderer, false);
|
||||||
|
view._renderer = renderer;
|
||||||
|
view.ApplyColor(false);
|
||||||
|
|
||||||
|
var collider = go.AddComponent<BoxCollider>();
|
||||||
|
collider.size = new Vector3(
|
||||||
|
config.cardWidth * 1.12f,
|
||||||
|
0.02f,
|
||||||
|
config.cardHeight * 1.12f);
|
||||||
|
collider.center = Vector3.zero;
|
||||||
|
|
||||||
|
var label = slot == PlaymatLayout.VanguardSlot ? "V" : "R";
|
||||||
|
var text = BoardText.Create(
|
||||||
|
go.transform,
|
||||||
|
label,
|
||||||
|
0.14f,
|
||||||
|
new Color(1f, 1f, 1f, 0.34f),
|
||||||
|
new Vector3(0f, 0.001f, 0f));
|
||||||
|
text.name = "Slot Label";
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHighlighted(bool highlighted)
|
||||||
|
{
|
||||||
|
ApplyColor(highlighted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyColor(bool highlighted)
|
||||||
|
{
|
||||||
|
if (_renderer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_renderer.GetPropertyBlock(_block);
|
||||||
|
_block.Clear();
|
||||||
|
BoardMaterials.SetColor(
|
||||||
|
_block,
|
||||||
|
_renderer.sharedMaterial,
|
||||||
|
highlighted ? _highlightColor : _baseColor);
|
||||||
|
_renderer.SetPropertyBlock(_block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 69d07f80399a37a4b9faef94f0c43fde
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Rendering;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>Плоские подписи на столе без TMP и Canvas.</summary>
|
||||||
|
internal static class BoardText
|
||||||
|
{
|
||||||
|
private static Font _font;
|
||||||
|
|
||||||
|
public static Font Font
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_font != null)
|
||||||
|
return _font;
|
||||||
|
|
||||||
|
_font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||||
|
if (_font == null)
|
||||||
|
_font = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||||
|
if (_font == null)
|
||||||
|
_font = Resources.GetBuiltinResource<Font>("Builtin-LegacyRuntime.ttf");
|
||||||
|
return _font;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TextMesh Create(
|
||||||
|
Transform parent,
|
||||||
|
string text,
|
||||||
|
float characterSize,
|
||||||
|
Color color,
|
||||||
|
Vector3 localPosition,
|
||||||
|
float yaw = 0f,
|
||||||
|
TextAnchor anchor = TextAnchor.MiddleCenter)
|
||||||
|
{
|
||||||
|
var go = new GameObject("Board Label");
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
go.transform.localPosition = localPosition;
|
||||||
|
// TextMesh по умолчанию смотрит в -Z; +90° по X кладёт его на стол.
|
||||||
|
go.transform.localRotation = Quaternion.Euler(90f, yaw, 0f);
|
||||||
|
|
||||||
|
var mesh = go.AddComponent<TextMesh>();
|
||||||
|
mesh.font = Font;
|
||||||
|
mesh.fontSize = 64;
|
||||||
|
mesh.characterSize = characterSize;
|
||||||
|
mesh.anchor = anchor;
|
||||||
|
mesh.alignment = TextAlignment.Center;
|
||||||
|
mesh.color = color;
|
||||||
|
mesh.text = text;
|
||||||
|
mesh.richText = false;
|
||||||
|
|
||||||
|
var renderer = go.GetComponent<MeshRenderer>();
|
||||||
|
if (mesh.font != null && mesh.font.material != null)
|
||||||
|
renderer.sharedMaterial = mesh.font.material;
|
||||||
|
renderer.shadowCastingMode = ShadowCastingMode.Off;
|
||||||
|
renderer.receiveShadows = false;
|
||||||
|
renderer.sortingOrder = 1;
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetText(TextMesh mesh, string text)
|
||||||
|
{
|
||||||
|
if (mesh != null)
|
||||||
|
mesh.text = text ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetColor(TextMesh mesh, Color color)
|
||||||
|
{
|
||||||
|
if (mesh != null)
|
||||||
|
mesh.color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5622bffba113d9843b24961dd5dbf81f
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>Процедурная рубашка и запасное лицо карты без внешних ассетов.</summary>
|
||||||
|
internal static class BoardTextures
|
||||||
|
{
|
||||||
|
private const int BackWidth = 256;
|
||||||
|
private const int BackHeight = 356;
|
||||||
|
|
||||||
|
private static Texture2D _back;
|
||||||
|
private static Texture2D _white;
|
||||||
|
|
||||||
|
public static Texture2D White
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_white == null)
|
||||||
|
{
|
||||||
|
_white = new Texture2D(2, 2, TextureFormat.RGBA32, false)
|
||||||
|
{
|
||||||
|
name = "Board White",
|
||||||
|
filterMode = FilterMode.Bilinear,
|
||||||
|
wrapMode = TextureWrapMode.Clamp,
|
||||||
|
hideFlags = HideFlags.DontSave,
|
||||||
|
};
|
||||||
|
_white.SetPixels(new[] { Color.white, Color.white, Color.white, Color.white });
|
||||||
|
_white.Apply(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Texture2D Back
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_back == null)
|
||||||
|
_back = BuildBack();
|
||||||
|
return _back;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color AccentFor(string printingId)
|
||||||
|
{
|
||||||
|
var hash = 2166136261u;
|
||||||
|
if (!string.IsNullOrEmpty(printingId))
|
||||||
|
{
|
||||||
|
foreach (var ch in printingId)
|
||||||
|
{
|
||||||
|
hash ^= ch;
|
||||||
|
hash *= 16777619u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hue = (hash % 360u) / 360f;
|
||||||
|
return Color.HSVToRGB(hue, 0.42f, 0.62f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Texture2D BuildBack()
|
||||||
|
{
|
||||||
|
var texture = new Texture2D(
|
||||||
|
BackWidth,
|
||||||
|
BackHeight,
|
||||||
|
TextureFormat.RGBA32,
|
||||||
|
false)
|
||||||
|
{
|
||||||
|
name = "Board Card Back",
|
||||||
|
filterMode = FilterMode.Bilinear,
|
||||||
|
wrapMode = TextureWrapMode.Clamp,
|
||||||
|
hideFlags = HideFlags.DontSave,
|
||||||
|
};
|
||||||
|
|
||||||
|
var pixels = new Color[BackWidth * BackHeight];
|
||||||
|
var dark = new Color(0.035f, 0.055f, 0.11f);
|
||||||
|
var mid = new Color(0.10f, 0.18f, 0.36f);
|
||||||
|
var edge = new Color(0.72f, 0.56f, 0.22f);
|
||||||
|
|
||||||
|
for (var y = 0; y < BackHeight; y++)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < BackWidth; x++)
|
||||||
|
{
|
||||||
|
var u = (x + 0.5f) / BackWidth;
|
||||||
|
var v = (y + 0.5f) / BackHeight;
|
||||||
|
var border = Mathf.Min(
|
||||||
|
Mathf.Min(u, 1f - u) * BackWidth,
|
||||||
|
Mathf.Min(v, 1f - v) * BackHeight);
|
||||||
|
|
||||||
|
var color = Color.Lerp(dark, mid, v * 0.55f + u * 0.15f);
|
||||||
|
var diagonal = Mathf.Abs(((u + v) % 0.5f) - 0.25f);
|
||||||
|
if (diagonal < 0.018f)
|
||||||
|
color = Color.Lerp(color, mid, 0.45f);
|
||||||
|
|
||||||
|
var dx = Mathf.Abs(u - 0.5f) * BackWidth;
|
||||||
|
var dy = Mathf.Abs(v - 0.5f) * BackHeight;
|
||||||
|
if (dx + dy < 34f)
|
||||||
|
color = Color.Lerp(color, edge, 0.65f);
|
||||||
|
|
||||||
|
if (border < 4f)
|
||||||
|
color = edge;
|
||||||
|
|
||||||
|
pixels[(y * BackWidth) + x] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
texture.SetPixels(pixels);
|
||||||
|
texture.Apply(false, true);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 41706474af643f54c81c3469dea25f40
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>Плоская зона стола: общий круг защиты, урон, душа.</summary>
|
||||||
|
public sealed class BoardZoneView : MonoBehaviour
|
||||||
|
{
|
||||||
|
private MeshRenderer _renderer;
|
||||||
|
private MaterialPropertyBlock _block;
|
||||||
|
private Color _baseColor;
|
||||||
|
private Color _highlightColor;
|
||||||
|
private TextMesh _label;
|
||||||
|
|
||||||
|
public static BoardZoneView Create(
|
||||||
|
Transform parent,
|
||||||
|
string name,
|
||||||
|
Vector3 position,
|
||||||
|
float width,
|
||||||
|
float depth,
|
||||||
|
float border,
|
||||||
|
Color color,
|
||||||
|
string label,
|
||||||
|
float labelSize,
|
||||||
|
float labelOffsetZ,
|
||||||
|
Color labelColor,
|
||||||
|
Color highlightColor)
|
||||||
|
{
|
||||||
|
var go = new GameObject(name);
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
go.transform.localPosition = position;
|
||||||
|
|
||||||
|
var view = go.AddComponent<BoardZoneView>();
|
||||||
|
view._baseColor = color;
|
||||||
|
view._highlightColor = highlightColor;
|
||||||
|
view._block = new MaterialPropertyBlock();
|
||||||
|
|
||||||
|
var filter = go.AddComponent<MeshFilter>();
|
||||||
|
filter.sharedMesh = BoardMeshLibrary.Frame(width, depth, border);
|
||||||
|
|
||||||
|
var renderer = go.AddComponent<MeshRenderer>();
|
||||||
|
renderer.sharedMaterial = BoardMaterials.Highlight;
|
||||||
|
BoardMaterials.SetShadows(renderer, false);
|
||||||
|
view._renderer = renderer;
|
||||||
|
view.ApplyColor(false);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(label))
|
||||||
|
{
|
||||||
|
view._label = BoardText.Create(
|
||||||
|
go.transform,
|
||||||
|
label,
|
||||||
|
labelSize,
|
||||||
|
labelColor,
|
||||||
|
new Vector3(0f, 0.002f, labelOffsetZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLabel(string text)
|
||||||
|
{
|
||||||
|
BoardText.SetText(_label, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHighlighted(bool highlighted)
|
||||||
|
{
|
||||||
|
ApplyColor(highlighted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetColor(Color color)
|
||||||
|
{
|
||||||
|
_baseColor = color;
|
||||||
|
ApplyColor(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyColor(bool highlighted)
|
||||||
|
{
|
||||||
|
if (_renderer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_renderer.GetPropertyBlock(_block);
|
||||||
|
_block.Clear();
|
||||||
|
BoardMaterials.SetColor(
|
||||||
|
_block,
|
||||||
|
_renderer.sharedMaterial,
|
||||||
|
highlighted ? _highlightColor : _baseColor);
|
||||||
|
_renderer.SetPropertyBlock(_block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b56ad9ea684c45d418241b20af51c21a
|
||||||
|
|
@ -0,0 +1,274 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Rendering;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Карта на столе: тонкая коробка с отдельными материалами лица, рубашки
|
||||||
|
/// и торца. Rest — поворот вокруг Y, закрытость — переворот вокруг X.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Card3D : MonoBehaviour
|
||||||
|
{
|
||||||
|
private PlaymatLayoutConfig _config;
|
||||||
|
private MeshRenderer _front;
|
||||||
|
private MeshRenderer _back;
|
||||||
|
private MeshRenderer _edge;
|
||||||
|
private MeshRenderer _highlight;
|
||||||
|
private BoxCollider _collider;
|
||||||
|
private MaterialPropertyBlock _block;
|
||||||
|
|
||||||
|
private bool _faceUp = true;
|
||||||
|
private bool _rested;
|
||||||
|
private Quaternion _baseRotation = Quaternion.identity;
|
||||||
|
private Quaternion _targetRotation = Quaternion.identity;
|
||||||
|
private Vector3 _targetPosition;
|
||||||
|
private Vector3 _targetScale = Vector3.one;
|
||||||
|
private bool _immediate;
|
||||||
|
|
||||||
|
private bool _dragging;
|
||||||
|
private Vector3 _dragOriginPosition;
|
||||||
|
private Quaternion _dragOriginRotation;
|
||||||
|
|
||||||
|
public BoardCardRef Ref { get; private set; }
|
||||||
|
public bool IsFaceUp => _faceUp;
|
||||||
|
public bool IsRested => _rested;
|
||||||
|
public bool IsDragging => _dragging;
|
||||||
|
public Vector3 TargetPosition => _targetPosition;
|
||||||
|
public float TargetYaw => _targetRotation.eulerAngles.y;
|
||||||
|
public Quaternion TargetRotation => _targetRotation;
|
||||||
|
|
||||||
|
public static Card3D Create(Transform parent, PlaymatLayoutConfig config)
|
||||||
|
{
|
||||||
|
var go = new GameObject("Card3D");
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
var card = go.AddComponent<Card3D>();
|
||||||
|
card.Build(config != null ? config : PlaymatLayoutConfig.Default);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Build(PlaymatLayoutConfig config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_block = new MaterialPropertyBlock();
|
||||||
|
|
||||||
|
var width = _config.cardWidth;
|
||||||
|
var height = _config.cardHeight;
|
||||||
|
var thickness = _config.cardThickness;
|
||||||
|
|
||||||
|
var edgeGo = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||||
|
edgeGo.name = "Edge";
|
||||||
|
RemoveCollider(edgeGo);
|
||||||
|
edgeGo.transform.SetParent(transform, false);
|
||||||
|
edgeGo.transform.localScale = new Vector3(width, thickness, height);
|
||||||
|
_edge = edgeGo.GetComponent<MeshRenderer>();
|
||||||
|
_edge.sharedMaterial = BoardMaterials.Edge;
|
||||||
|
BoardMaterials.SetShadows(_edge, true);
|
||||||
|
|
||||||
|
var frontGo = new GameObject("Front");
|
||||||
|
frontGo.transform.SetParent(transform, false);
|
||||||
|
frontGo.transform.localPosition = new Vector3(0f, (thickness * 0.5f) + 0.0008f, 0f);
|
||||||
|
var frontFilter = frontGo.AddComponent<MeshFilter>();
|
||||||
|
frontFilter.sharedMesh = BoardMeshLibrary.CardFace(width, height, front: true);
|
||||||
|
_front = frontGo.AddComponent<MeshRenderer>();
|
||||||
|
_front.sharedMaterial = BoardMaterials.White;
|
||||||
|
BoardMaterials.SetShadows(_front, false);
|
||||||
|
|
||||||
|
var backGo = new GameObject("Back");
|
||||||
|
backGo.transform.SetParent(transform, false);
|
||||||
|
backGo.transform.localPosition = new Vector3(0f, -(thickness * 0.5f) - 0.0008f, 0f);
|
||||||
|
var backFilter = backGo.AddComponent<MeshFilter>();
|
||||||
|
backFilter.sharedMesh = BoardMeshLibrary.CardFace(width, height, front: false);
|
||||||
|
_back = backGo.AddComponent<MeshRenderer>();
|
||||||
|
_back.sharedMaterial = BoardMaterials.Back;
|
||||||
|
BoardMaterials.SetShadows(_back, false);
|
||||||
|
|
||||||
|
var highlightGo = new GameObject("Highlight");
|
||||||
|
highlightGo.transform.SetParent(transform, false);
|
||||||
|
highlightGo.transform.localPosition = new Vector3(0f, -(thickness * 0.5f) - 0.0015f, 0f);
|
||||||
|
var filter = highlightGo.AddComponent<MeshFilter>();
|
||||||
|
filter.sharedMesh = BoardMeshLibrary.Frame(
|
||||||
|
width * 1.16f,
|
||||||
|
height * 1.16f,
|
||||||
|
Mathf.Max(0.025f, width * 0.035f));
|
||||||
|
_highlight = highlightGo.AddComponent<MeshRenderer>();
|
||||||
|
_highlight.sharedMaterial = BoardMaterials.Highlight;
|
||||||
|
BoardMaterials.SetShadows(_highlight, false);
|
||||||
|
_highlight.gameObject.SetActive(false);
|
||||||
|
|
||||||
|
_collider = gameObject.AddComponent<BoxCollider>();
|
||||||
|
_collider.size = new Vector3(
|
||||||
|
width * 1.02f,
|
||||||
|
Mathf.Max(thickness + 0.02f, 0.03f),
|
||||||
|
height * 1.02f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Bind(BoardCardRef reference)
|
||||||
|
{
|
||||||
|
Ref = reference;
|
||||||
|
name = "Card3D " + reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetArt(Texture2D texture, Color accent)
|
||||||
|
{
|
||||||
|
if (_front == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_front.GetPropertyBlock(_block);
|
||||||
|
_block.Clear();
|
||||||
|
BoardMaterials.SetTexture(
|
||||||
|
_block,
|
||||||
|
_front.sharedMaterial,
|
||||||
|
texture != null ? texture : BoardTextures.White);
|
||||||
|
BoardMaterials.SetColor(
|
||||||
|
_block,
|
||||||
|
_front.sharedMaterial,
|
||||||
|
texture != null ? Color.white : accent);
|
||||||
|
_front.SetPropertyBlock(_block);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetState(Quaternion baseRotation, bool faceUp, bool rested)
|
||||||
|
{
|
||||||
|
_baseRotation = baseRotation;
|
||||||
|
_faceUp = faceUp;
|
||||||
|
_rested = rested;
|
||||||
|
|
||||||
|
var rest = Quaternion.Euler(0f, rested ? 90f : 0f, 0f);
|
||||||
|
var flip = Quaternion.Euler(faceUp ? 0f : 180f, 0f, 0f);
|
||||||
|
_targetRotation = rest * baseRotation * flip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPose(
|
||||||
|
Vector3 position,
|
||||||
|
Quaternion baseRotation,
|
||||||
|
bool faceUp,
|
||||||
|
bool rested,
|
||||||
|
bool immediate)
|
||||||
|
{
|
||||||
|
SetState(baseRotation, faceUp, rested);
|
||||||
|
_targetPosition = position;
|
||||||
|
_targetScale = Vector3.one;
|
||||||
|
_immediate = immediate;
|
||||||
|
|
||||||
|
if (_dragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (immediate)
|
||||||
|
Snap();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetVisible(bool visible)
|
||||||
|
{
|
||||||
|
gameObject.SetActive(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHighlight(Color? color)
|
||||||
|
{
|
||||||
|
if (_highlight == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var active = color.HasValue;
|
||||||
|
_highlight.gameObject.SetActive(active);
|
||||||
|
if (!active)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_highlight.GetPropertyBlock(_block);
|
||||||
|
_block.Clear();
|
||||||
|
BoardMaterials.SetColor(_block, _highlight.sharedMaterial, color.Value);
|
||||||
|
_highlight.SetPropertyBlock(_block);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeginDrag()
|
||||||
|
{
|
||||||
|
if (_dragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_dragging = true;
|
||||||
|
_dragOriginPosition = transform.position;
|
||||||
|
_dragOriginRotation = transform.rotation;
|
||||||
|
_targetScale = Vector3.one * 1.05f;
|
||||||
|
if (_collider != null)
|
||||||
|
_collider.enabled = false;
|
||||||
|
SetHighlight(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragTo(Vector3 worldPosition)
|
||||||
|
{
|
||||||
|
if (!_dragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var yaw = _targetRotation.eulerAngles.y;
|
||||||
|
transform.position = new Vector3(
|
||||||
|
worldPosition.x,
|
||||||
|
_config.dragLift + (_config.cardThickness * 0.5f),
|
||||||
|
worldPosition.z);
|
||||||
|
transform.rotation = Quaternion.Euler(0f, yaw, 0f);
|
||||||
|
transform.localScale = Vector3.one * 1.05f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DropAt(Vector3 position, Quaternion rotation)
|
||||||
|
{
|
||||||
|
_dragging = false;
|
||||||
|
if (_collider != null)
|
||||||
|
_collider.enabled = true;
|
||||||
|
_targetPosition = position;
|
||||||
|
_targetRotation = rotation;
|
||||||
|
_targetScale = Vector3.one;
|
||||||
|
_immediate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReturnToOrigin()
|
||||||
|
{
|
||||||
|
_dragging = false;
|
||||||
|
if (_collider != null)
|
||||||
|
_collider.enabled = true;
|
||||||
|
_targetPosition = _dragOriginPosition;
|
||||||
|
_targetRotation = _dragOriginRotation;
|
||||||
|
_targetScale = Vector3.one;
|
||||||
|
_immediate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Snap()
|
||||||
|
{
|
||||||
|
transform.position = _targetPosition;
|
||||||
|
transform.rotation = _targetRotation;
|
||||||
|
transform.localScale = _targetScale;
|
||||||
|
_immediate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Tick(float deltaTime)
|
||||||
|
{
|
||||||
|
if (_dragging || deltaTime <= 0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_immediate)
|
||||||
|
{
|
||||||
|
Snap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var move = 1f - Mathf.Exp(-_config.smoothMoveSpeed * deltaTime);
|
||||||
|
var rotate = 1f - Mathf.Exp(-_config.smoothRotateSpeed * deltaTime);
|
||||||
|
transform.position = Vector3.Lerp(transform.position, _targetPosition, move);
|
||||||
|
transform.rotation = Quaternion.Slerp(transform.rotation, _targetRotation, rotate);
|
||||||
|
transform.localScale = Vector3.Lerp(transform.localScale, _targetScale, move);
|
||||||
|
|
||||||
|
if (Vector3.SqrMagnitude(transform.position - _targetPosition) < 0.0000004f
|
||||||
|
&& Quaternion.Angle(transform.rotation, _targetRotation) < 0.1f)
|
||||||
|
{
|
||||||
|
Snap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveCollider(GameObject target)
|
||||||
|
{
|
||||||
|
var collider = target.GetComponent<Collider>();
|
||||||
|
if (collider == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Application.isPlaying)
|
||||||
|
Destroy(collider);
|
||||||
|
else
|
||||||
|
DestroyImmediate(collider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a8b5670bd4e091143b3d4752c0bb2b1a
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 178189a769df10042b9a76f8a2b7bb84
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Vanguard.Board3D
|
||||||
|
{
|
||||||
|
/// <summary>Колода, дроп или душа: стопка карт с подписью и коллайдером.</summary>
|
||||||
|
public sealed class Pile3D : MonoBehaviour
|
||||||
|
{
|
||||||
|
private PlaymatLayoutConfig _config;
|
||||||
|
private readonly GameObject[] _layers = new GameObject[3];
|
||||||
|
private MeshRenderer _top;
|
||||||
|
private MaterialPropertyBlock _block;
|
||||||
|
private TextMesh _label;
|
||||||
|
private int _count;
|
||||||
|
|
||||||
|
public BoardZone Zone { get; private set; }
|
||||||
|
public int Player { get; private set; }
|
||||||
|
public BoardCardRef Ref => BoardCardRef.Pile(Zone, Player, _count);
|
||||||
|
|
||||||
|
public void SetPlayer(int player)
|
||||||
|
{
|
||||||
|
Player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Pile3D Create(
|
||||||
|
Transform parent,
|
||||||
|
PlaymatLayoutConfig config,
|
||||||
|
BoardZone zone,
|
||||||
|
int player,
|
||||||
|
Vector3 position,
|
||||||
|
string title,
|
||||||
|
Color accent)
|
||||||
|
{
|
||||||
|
var go = new GameObject("Pile " + zone);
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
go.transform.localPosition = position;
|
||||||
|
|
||||||
|
var pile = go.AddComponent<Pile3D>();
|
||||||
|
pile.Zone = zone;
|
||||||
|
pile.Player = player;
|
||||||
|
pile._config = config;
|
||||||
|
pile._block = new MaterialPropertyBlock();
|
||||||
|
pile.Build(title, accent, config);
|
||||||
|
return pile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Build(string title, Color accent, PlaymatLayoutConfig config)
|
||||||
|
{
|
||||||
|
var width = config.cardWidth;
|
||||||
|
var height = config.cardHeight;
|
||||||
|
var thickness = Mathf.Max(0.006f, config.cardThickness * 1.2f);
|
||||||
|
|
||||||
|
for (var i = 0; i < _layers.Length; i++)
|
||||||
|
{
|
||||||
|
var layer = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||||
|
layer.name = "Layer " + i;
|
||||||
|
RemoveCollider(layer);
|
||||||
|
layer.transform.SetParent(transform, false);
|
||||||
|
layer.transform.localPosition = new Vector3(0f, i * thickness, 0f);
|
||||||
|
layer.transform.localScale = new Vector3(width, thickness, height);
|
||||||
|
var renderer = layer.GetComponent<MeshRenderer>();
|
||||||
|
renderer.sharedMaterial = Zone == BoardZone.Soul
|
||||||
|
? BoardMaterials.CreateUnlit(null, accent)
|
||||||
|
: BoardMaterials.Back;
|
||||||
|
BoardMaterials.SetShadows(renderer, true);
|
||||||
|
_layers[i] = layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
var topGo = new GameObject("Top");
|
||||||
|
topGo.transform.SetParent(transform, false);
|
||||||
|
var topFilter = topGo.AddComponent<MeshFilter>();
|
||||||
|
topFilter.sharedMesh = BoardMeshLibrary.CardFace(width, height, front: true);
|
||||||
|
_top = topGo.AddComponent<MeshRenderer>();
|
||||||
|
_top.sharedMaterial = BoardMaterials.White;
|
||||||
|
BoardMaterials.SetShadows(_top, false);
|
||||||
|
|
||||||
|
_label = BoardText.Create(
|
||||||
|
transform,
|
||||||
|
title,
|
||||||
|
0.115f,
|
||||||
|
new Color(0.86f, 0.90f, 1f),
|
||||||
|
new Vector3(0f, 0.02f, -(height * 0.78f)));
|
||||||
|
|
||||||
|
var collider = gameObject.AddComponent<BoxCollider>();
|
||||||
|
collider.size = new Vector3(width * 1.06f, 0.16f, height * 1.06f);
|
||||||
|
collider.center = new Vector3(0f, 0.08f, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCount(int count, Texture2D topTexture, string title, Color accent)
|
||||||
|
{
|
||||||
|
_count = count;
|
||||||
|
var safe = Mathf.Max(0, count);
|
||||||
|
var layers = safe <= 0 ? 1 : safe < 5 ? 1 : safe < 20 ? 2 : 3;
|
||||||
|
if (Zone == BoardZone.Soul)
|
||||||
|
layers = safe < 10 ? 1 : 2;
|
||||||
|
var thickness = Mathf.Max(0.006f, _config.cardThickness * 1.2f);
|
||||||
|
|
||||||
|
for (var i = 0; i < _layers.Length; i++)
|
||||||
|
_layers[i].SetActive(i < layers);
|
||||||
|
|
||||||
|
_top.transform.localPosition = new Vector3(
|
||||||
|
0f,
|
||||||
|
(layers - 1) * thickness + 0.0015f,
|
||||||
|
0f);
|
||||||
|
|
||||||
|
if (topTexture != null)
|
||||||
|
{
|
||||||
|
_top.GetPropertyBlock(_block);
|
||||||
|
_block.Clear();
|
||||||
|
BoardMaterials.SetTexture(_block, _top.sharedMaterial, topTexture);
|
||||||
|
BoardMaterials.SetColor(_block, _top.sharedMaterial, Color.white);
|
||||||
|
_top.SetPropertyBlock(_block);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_top.GetPropertyBlock(_block);
|
||||||
|
_block.Clear();
|
||||||
|
if (Zone == BoardZone.Soul)
|
||||||
|
{
|
||||||
|
BoardMaterials.SetTexture(_block, _top.sharedMaterial, BoardTextures.White);
|
||||||
|
BoardMaterials.SetColor(_block, _top.sharedMaterial, accent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BoardMaterials.SetTexture(_block, _top.sharedMaterial, BoardTextures.Back);
|
||||||
|
BoardMaterials.SetColor(_block, _top.sharedMaterial, Color.white);
|
||||||
|
}
|
||||||
|
|
||||||
|
_top.SetPropertyBlock(_block);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoardText.SetText(_label, title + " " + safe);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveCollider(GameObject target)
|
||||||
|
{
|
||||||
|
var collider = target.GetComponent<Collider>();
|
||||||
|
if (collider == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Application.isPlaying)
|
||||||
|
Destroy(collider);
|
||||||
|
else
|
||||||
|
DestroyImmediate(collider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2cbf86187ded1564ea87365f921f8be5
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -21,25 +21,26 @@ namespace Vanguard.UI
|
||||||
if (target == null)
|
if (target == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
target.sprite = fallback;
|
var sprite = Resolve(imageKey, fallback);
|
||||||
|
target.sprite = sprite;
|
||||||
target.color = Color.white;
|
target.color = Color.white;
|
||||||
target.preserveAspect = false;
|
target.preserveAspect = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Спрайт реального арта или процедурный fallback.</summary>
|
||||||
|
public static Sprite Resolve(string imageKey, Sprite fallback)
|
||||||
|
{
|
||||||
if (string.IsNullOrWhiteSpace(imageKey))
|
if (string.IsNullOrWhiteSpace(imageKey))
|
||||||
return;
|
return fallback;
|
||||||
|
|
||||||
if (Cache.TryGetValue(imageKey, out var cached))
|
if (Cache.TryGetValue(imageKey, out var cached))
|
||||||
{
|
return cached != null ? cached : fallback;
|
||||||
if (cached != null)
|
|
||||||
target.sprite = cached;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var texture = Resources.Load<Texture2D>(ResourceFolder + imageKey);
|
var texture = Resources.Load<Texture2D>(ResourceFolder + imageKey);
|
||||||
if (texture == null)
|
if (texture == null)
|
||||||
{
|
{
|
||||||
Cache[imageKey] = null;
|
Cache[imageKey] = null;
|
||||||
return;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sprite = Sprite.Create(
|
var sprite = Sprite.Create(
|
||||||
|
|
@ -48,7 +49,7 @@ namespace Vanguard.UI
|
||||||
new Vector2(0.5f, 0.5f),
|
new Vector2(0.5f, 0.5f),
|
||||||
100f);
|
100f);
|
||||||
Cache[imageKey] = sprite;
|
Cache[imageKey] = sprite;
|
||||||
target.sprite = sprite;
|
return sprite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,354 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!29 &1
|
||||||
|
OcclusionCullingSettings:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 2
|
||||||
|
m_OcclusionBakeSettings:
|
||||||
|
smallestOccluder: 5
|
||||||
|
smallestHole: 0.25
|
||||||
|
backfaceThreshold: 100
|
||||||
|
m_SceneGUID: 00000000000000000000000000000000
|
||||||
|
m_OcclusionCullingData: {fileID: 0}
|
||||||
|
--- !u!104 &2
|
||||||
|
RenderSettings:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 10
|
||||||
|
m_Fog: 0
|
||||||
|
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
||||||
|
m_FogMode: 3
|
||||||
|
m_FogDensity: 0.01
|
||||||
|
m_LinearFogStart: 0
|
||||||
|
m_LinearFogEnd: 300
|
||||||
|
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
|
||||||
|
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
|
||||||
|
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
|
||||||
|
m_AmbientIntensity: 1
|
||||||
|
m_AmbientMode: 0
|
||||||
|
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
|
||||||
|
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_HaloStrength: 0.5
|
||||||
|
m_FlareStrength: 1
|
||||||
|
m_FlareFadeSpeed: 3
|
||||||
|
m_HaloTexture: {fileID: 0}
|
||||||
|
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
m_DefaultReflectionMode: 0
|
||||||
|
m_DefaultReflectionResolution: 128
|
||||||
|
m_ReflectionBounces: 1
|
||||||
|
m_ReflectionIntensity: 1
|
||||||
|
m_CustomReflection: {fileID: 0}
|
||||||
|
m_Sun: {fileID: 0}
|
||||||
|
m_UseRadianceAmbientProbe: 0
|
||||||
|
--- !u!157 &3
|
||||||
|
LightmapSettings:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 13
|
||||||
|
m_BakeOnSceneLoad: 0
|
||||||
|
m_GISettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_BounceScale: 1
|
||||||
|
m_IndirectOutputScale: 1
|
||||||
|
m_AlbedoBoost: 1
|
||||||
|
m_EnvironmentLightingMode: 0
|
||||||
|
m_EnableBakedLightmaps: 1
|
||||||
|
m_EnableRealtimeLightmaps: 0
|
||||||
|
m_LightmapEditorSettings:
|
||||||
|
serializedVersion: 12
|
||||||
|
m_Resolution: 2
|
||||||
|
m_BakeResolution: 40
|
||||||
|
m_AtlasSize: 1024
|
||||||
|
m_AO: 0
|
||||||
|
m_AOMaxDistance: 1
|
||||||
|
m_CompAOExponent: 1
|
||||||
|
m_CompAOExponentDirect: 0
|
||||||
|
m_ExtractAmbientOcclusion: 0
|
||||||
|
m_Padding: 2
|
||||||
|
m_LightmapParameters: {fileID: 0}
|
||||||
|
m_LightmapsBakeMode: 1
|
||||||
|
m_TextureCompression: 1
|
||||||
|
m_ReflectionCompression: 2
|
||||||
|
m_MixedBakeMode: 2
|
||||||
|
m_BakeBackend: 2
|
||||||
|
m_PVRSampling: 1
|
||||||
|
m_PVRDirectSampleCount: 32
|
||||||
|
m_PVRSampleCount: 512
|
||||||
|
m_PVRBounces: 2
|
||||||
|
m_PVREnvironmentSampleCount: 256
|
||||||
|
m_PVREnvironmentReferencePointCount: 2048
|
||||||
|
m_PVRFilteringMode: 1
|
||||||
|
m_PVRDenoiserTypeDirect: 2
|
||||||
|
m_PVRDenoiserTypeIndirect: 2
|
||||||
|
m_PVRDenoiserTypeAO: 2
|
||||||
|
m_PVRFilterTypeDirect: 0
|
||||||
|
m_PVRFilterTypeIndirect: 0
|
||||||
|
m_PVRFilterTypeAO: 0
|
||||||
|
m_PVREnvironmentMIS: 1
|
||||||
|
m_PVRCulling: 1
|
||||||
|
m_PVRFilteringGaussRadiusDirect: 1
|
||||||
|
m_PVRFilteringGaussRadiusIndirect: 1
|
||||||
|
m_PVRFilteringGaussRadiusAO: 1
|
||||||
|
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
|
||||||
|
m_PVRFilteringAtrousPositionSigmaIndirect: 2
|
||||||
|
m_PVRFilteringAtrousPositionSigmaAO: 1
|
||||||
|
m_ExportTrainingData: 0
|
||||||
|
m_TrainingDataDestination: TrainingData
|
||||||
|
m_LightProbeSampleCountMultiplier: 4
|
||||||
|
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_LightingSettings: {fileID: 0}
|
||||||
|
--- !u!196 &4
|
||||||
|
NavMeshSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_BuildSettings:
|
||||||
|
serializedVersion: 3
|
||||||
|
agentTypeID: 0
|
||||||
|
agentRadius: 0.5
|
||||||
|
agentHeight: 2
|
||||||
|
agentSlope: 45
|
||||||
|
agentClimb: 0.4
|
||||||
|
ledgeDropHeight: 0
|
||||||
|
maxJumpAcrossDistance: 0
|
||||||
|
minRegionArea: 2
|
||||||
|
manualCellSize: 0
|
||||||
|
cellSize: 0.16666667
|
||||||
|
manualTileSize: 0
|
||||||
|
tileSize: 256
|
||||||
|
buildHeightMesh: 0
|
||||||
|
maxJobWorkers: 0
|
||||||
|
preserveTilesOutsideBounds: 0
|
||||||
|
debug:
|
||||||
|
m_Flags: 0
|
||||||
|
m_NavMeshData: {fileID: 0}
|
||||||
|
--- !u!1 &101028085
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 101028087}
|
||||||
|
- component: {fileID: 101028086}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Directional Light
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!108 &101028086
|
||||||
|
Light:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 101028085}
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 13
|
||||||
|
m_Type: 1
|
||||||
|
m_Color: {r: 1, g: 0.88, b: 0.68, a: 1}
|
||||||
|
m_Intensity: 1.15
|
||||||
|
m_Range: 10
|
||||||
|
m_SpotAngle: 30
|
||||||
|
m_InnerSpotAngle: 21.80208
|
||||||
|
m_CookieSize2D: {x: 0.5, y: 0.5}
|
||||||
|
m_Shadows:
|
||||||
|
m_Type: 2
|
||||||
|
m_Resolution: -1
|
||||||
|
m_CustomResolution: -1
|
||||||
|
m_Strength: 1
|
||||||
|
m_Bias: 0.05
|
||||||
|
m_NormalBias: 0.4
|
||||||
|
m_NearPlane: 0.2
|
||||||
|
m_CullingMatrixOverride:
|
||||||
|
e00: 1
|
||||||
|
e01: 0
|
||||||
|
e02: 0
|
||||||
|
e03: 0
|
||||||
|
e10: 0
|
||||||
|
e11: 1
|
||||||
|
e12: 0
|
||||||
|
e13: 0
|
||||||
|
e20: 0
|
||||||
|
e21: 0
|
||||||
|
e22: 1
|
||||||
|
e23: 0
|
||||||
|
e30: 0
|
||||||
|
e31: 0
|
||||||
|
e32: 0
|
||||||
|
e33: 1
|
||||||
|
m_UseCullingMatrixOverride: 0
|
||||||
|
m_Cookie: {fileID: 0}
|
||||||
|
m_DrawHalo: 0
|
||||||
|
m_Flare: {fileID: 0}
|
||||||
|
m_RenderMode: 0
|
||||||
|
m_CullingMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 4294967295
|
||||||
|
m_RenderingLayerMask: 1
|
||||||
|
m_Lightmapping: 4
|
||||||
|
m_LightShadowCasterMode: 0
|
||||||
|
m_AreaSize: {x: 1, y: 1}
|
||||||
|
m_BounceIntensity: 1
|
||||||
|
m_ColorTemperature: 6570
|
||||||
|
m_UseColorTemperature: 0
|
||||||
|
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
m_UseBoundingSphereOverride: 0
|
||||||
|
m_UseViewFrustumForShadowCasterCull: 1
|
||||||
|
m_ForceVisible: 0
|
||||||
|
m_ShapeRadius: 0.025
|
||||||
|
m_ShadowAngle: 0
|
||||||
|
m_LightUnit: 1
|
||||||
|
m_LuxAtDistance: 1
|
||||||
|
m_EnableSpotReflector: 1
|
||||||
|
--- !u!4 &101028087
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 101028085}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0.39098036, y: -0.25180724, z: 0.112111814, w: 0.87815624}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &320665827
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 320665829}
|
||||||
|
- component: {fileID: 320665828}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Main Camera
|
||||||
|
m_TagString: MainCamera
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!20 &320665828
|
||||||
|
Camera:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 320665827}
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 2
|
||||||
|
m_ClearFlags: 2
|
||||||
|
m_BackGroundColor: {r: 0.02, g: 0.025, b: 0.045, a: 1}
|
||||||
|
m_projectionMatrixMode: 1
|
||||||
|
m_GateFitMode: 2
|
||||||
|
m_FOVAxisMode: 0
|
||||||
|
m_Iso: 200
|
||||||
|
m_ShutterSpeed: 0.005
|
||||||
|
m_Aperture: 16
|
||||||
|
m_FocusDistance: 10
|
||||||
|
m_FocalLength: 50
|
||||||
|
m_BladeCount: 5
|
||||||
|
m_Curvature: {x: 2, y: 11}
|
||||||
|
m_BarrelClipping: 0.25
|
||||||
|
m_Anamorphism: 0
|
||||||
|
m_SensorSize: {x: 36, y: 24}
|
||||||
|
m_LensShift: {x: 0, y: 0}
|
||||||
|
m_NormalizedViewPortRect:
|
||||||
|
serializedVersion: 2
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: 1
|
||||||
|
height: 1
|
||||||
|
near clip plane: 0.3
|
||||||
|
far clip plane: 1000
|
||||||
|
field of view: 50
|
||||||
|
orthographic: 0
|
||||||
|
orthographic size: 5
|
||||||
|
m_Depth: 0
|
||||||
|
m_CullingMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 4294967295
|
||||||
|
m_RenderingPath: -1
|
||||||
|
m_TargetTexture: {fileID: 0}
|
||||||
|
m_TargetDisplay: 0
|
||||||
|
m_TargetEye: 3
|
||||||
|
m_HDR: 1
|
||||||
|
m_AllowMSAA: 1
|
||||||
|
m_AllowDynamicResolution: 0
|
||||||
|
m_ForceIntoRT: 0
|
||||||
|
m_OcclusionCulling: 1
|
||||||
|
m_StereoConvergence: 10
|
||||||
|
m_StereoSeparation: 0.022
|
||||||
|
--- !u!4 &320665829
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 320665827}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0.29581374, y: 0, z: 0, w: 0.9552457}
|
||||||
|
m_LocalPosition: {x: 0, y: 8.7, z: -12.4}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &1168794765
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1168794767}
|
||||||
|
- component: {fileID: 1168794766}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Playmat Sandbox
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!114 &1168794766
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1168794765}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 89fedfc3c1f371c448b4187e210d36fb, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Vanguard.Runtime::Vanguard.Board3D.PlaymatSandbox
|
||||||
|
config: {fileID: 11400000, guid: c1f4dc13d22d0004ba1bddedecf71461, type: 2}
|
||||||
|
showHints: 1
|
||||||
|
--- !u!4 &1168794767
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1168794765}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1660057539 &9223372036854775807
|
||||||
|
SceneRoots:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_Roots:
|
||||||
|
- {fileID: 320665829}
|
||||||
|
- {fileID: 101028087}
|
||||||
|
- {fileID: 1168794767}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3282ed3425597474dbc400ae4316780f
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -9,6 +9,7 @@ using Vanguard.Api;
|
||||||
using Vanguard.Api.Contracts;
|
using Vanguard.Api.Contracts;
|
||||||
using Vanguard.App;
|
using Vanguard.App;
|
||||||
using Vanguard.Auth;
|
using Vanguard.Auth;
|
||||||
|
using Vanguard.Board3D;
|
||||||
using Vanguard.Meta;
|
using Vanguard.Meta;
|
||||||
using Vanguard.Shared.Protocol;
|
using Vanguard.Shared.Protocol;
|
||||||
using Vanguard.State;
|
using Vanguard.State;
|
||||||
|
|
@ -120,7 +121,7 @@ namespace Vanguard.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Battle_Renders_Damage_And_Rested_Cards()
|
public void Battle_Renders_3D_Damage_And_Rested_Cards()
|
||||||
{
|
{
|
||||||
var config = new AppConfig();
|
var config = new AppConfig();
|
||||||
config.Load();
|
config.Load();
|
||||||
|
|
@ -192,6 +193,10 @@ namespace Vanguard.Tests
|
||||||
LegalIntents = new List<IntentPayload>(),
|
LegalIntents = new List<IntentPayload>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Resources.FindObjectsOfTypeAll<GameBoard3D>()
|
||||||
|
.FirstOrDefault()
|
||||||
|
?.SetVisible(true);
|
||||||
|
|
||||||
typeof(BattleView)
|
typeof(BattleView)
|
||||||
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!
|
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
.SetValue(battle, match);
|
.SetValue(battle, match);
|
||||||
|
|
@ -199,21 +204,30 @@ namespace Vanguard.Tests
|
||||||
.GetMethod("Render", BindingFlags.Instance | BindingFlags.NonPublic)!
|
.GetMethod("Render", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
.Invoke(battle, null);
|
.Invoke(battle, null);
|
||||||
|
|
||||||
Assert.IsTrue(Exists("DamageZone"));
|
Assert.IsTrue(Exists("GameBoard3D"));
|
||||||
Assert.IsTrue(Exists("Damage_0"));
|
|
||||||
Assert.IsTrue(Exists("Damage_1"));
|
var cards = Resources.FindObjectsOfTypeAll<Card3D>()
|
||||||
Assert.IsTrue(Exists("Damage_2"));
|
.Where(card => card.gameObject.activeInHierarchy)
|
||||||
Assert.IsTrue(Exists("Guard_7"));
|
.ToList();
|
||||||
Assert.IsTrue(Exists("Card_R4"));
|
Assert.GreaterOrEqual(cards.Count, 3);
|
||||||
var hasSidewaysCard = Resources.FindObjectsOfTypeAll<GameObject>()
|
|
||||||
.Where(go => go.name == "Card_R4")
|
var rested = cards.FirstOrDefault(card =>
|
||||||
.Any(go =>
|
card.Ref.Zone == BoardZone.RearGuard
|
||||||
{
|
&& card.Ref.Player == 0
|
||||||
var angle = go.transform.localEulerAngles.z;
|
&& card.Ref.Slot == 3);
|
||||||
return Mathf.Abs(Mathf.DeltaAngle(angle, 90f)) < 1f
|
Assert.IsNotNull(rested, "Rested rear-guard was not rendered in 3D");
|
||||||
|| Mathf.Abs(Mathf.DeltaAngle(angle, -90f)) < 1f;
|
Assert.IsTrue(rested.IsRested);
|
||||||
});
|
Assert.AreEqual(90f, Mathf.Abs(Mathf.DeltaAngle(rested.TargetYaw, 0f)), 0.1f);
|
||||||
Assert.IsTrue(hasSidewaysCard, "Rested card was not turned sideways");
|
|
||||||
|
var damage = cards.Where(card =>
|
||||||
|
card.Ref.Zone == BoardZone.Damage
|
||||||
|
&& card.Ref.Player == 0).ToList();
|
||||||
|
Assert.AreEqual(3, damage.Count);
|
||||||
|
Assert.IsFalse(damage.Single(card => card.Ref.Index == 2).IsFaceUp);
|
||||||
|
|
||||||
|
Assert.IsTrue(cards.Any(card =>
|
||||||
|
card.Ref.Zone == BoardZone.Guardian
|
||||||
|
&& card.Ref.InstanceId == 7));
|
||||||
|
|
||||||
Cleanup();
|
Cleanup();
|
||||||
}
|
}
|
||||||
|
|
@ -223,6 +237,12 @@ namespace Vanguard.Tests
|
||||||
|
|
||||||
private static void Cleanup()
|
private static void Cleanup()
|
||||||
{
|
{
|
||||||
|
foreach (var board in Resources.FindObjectsOfTypeAll<GameBoard3D>())
|
||||||
|
{
|
||||||
|
if (board != null)
|
||||||
|
Object.DestroyImmediate(board.gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
var canvas = GameObject.Find("Test Canvas");
|
var canvas = GameObject.Find("Test Canvas");
|
||||||
if (canvas != null)
|
if (canvas != null)
|
||||||
Object.DestroyImmediate(canvas);
|
Object.DestroyImmediate(canvas);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
using Vanguard.Board3D;
|
||||||
|
using Vanguard.Shared.Protocol;
|
||||||
|
|
||||||
|
namespace Vanguard.Tests
|
||||||
|
{
|
||||||
|
public sealed class GameBoard3DTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Board_Renders_Field_Hand_Guardian_Damage_And_Soul()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var board = GameBoard3D.Create(null, config);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
board.SetVisible(true);
|
||||||
|
board.Render(BuildView(you: 0), new BoardRenderState());
|
||||||
|
|
||||||
|
var cards = ActiveCards();
|
||||||
|
Assert.GreaterOrEqual(cards.Count, 9);
|
||||||
|
|
||||||
|
Assert.IsTrue(cards.Any(card =>
|
||||||
|
card.Ref.Zone == BoardZone.RearGuard
|
||||||
|
&& card.Ref.Player == 0
|
||||||
|
&& card.Ref.Slot == 3));
|
||||||
|
Assert.IsTrue(cards.Any(card =>
|
||||||
|
card.Ref.Zone == BoardZone.Guardian
|
||||||
|
&& card.Ref.InstanceId == 7));
|
||||||
|
Assert.IsTrue(cards.Any(card =>
|
||||||
|
card.Ref.Zone == BoardZone.Hand
|
||||||
|
&& card.Ref.Player == 0));
|
||||||
|
Assert.IsTrue(cards.Any(card =>
|
||||||
|
card.Ref.Zone == BoardZone.Damage
|
||||||
|
&& card.Ref.Player == 0));
|
||||||
|
|
||||||
|
var rested = cards.Single(card =>
|
||||||
|
card.Ref.Zone == BoardZone.RearGuard
|
||||||
|
&& card.Ref.Player == 0
|
||||||
|
&& card.Ref.Slot == 3);
|
||||||
|
Assert.IsTrue(rested.IsRested);
|
||||||
|
Assert.AreEqual(90f, Mathf.Abs(Mathf.DeltaAngle(rested.TargetYaw, 0f)), 0.1f);
|
||||||
|
|
||||||
|
var hiddenDamage = cards.Single(card =>
|
||||||
|
card.Ref.Zone == BoardZone.Damage
|
||||||
|
&& card.Ref.Player == 0
|
||||||
|
&& card.Ref.Index == 1);
|
||||||
|
Assert.IsFalse(hiddenDamage.IsFaceUp);
|
||||||
|
Assert.IsTrue(hiddenDamage.IsRested);
|
||||||
|
|
||||||
|
var visibleDamage = cards.Single(card =>
|
||||||
|
card.Ref.Zone == BoardZone.Damage
|
||||||
|
&& card.Ref.Player == 0
|
||||||
|
&& card.Ref.Index == 0);
|
||||||
|
Assert.IsTrue(visibleDamage.IsFaceUp);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Object.DestroyImmediate(board.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Pool_Reuses_Cards_Between_Snapshots()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var board = GameBoard3D.Create(null, config);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
board.SetVisible(true);
|
||||||
|
var view = BuildView(you: 0);
|
||||||
|
board.Render(view, new BoardRenderState());
|
||||||
|
var count = ActiveCards().Count;
|
||||||
|
|
||||||
|
board.Render(view, new BoardRenderState());
|
||||||
|
Assert.AreEqual(count, ActiveCards().Count);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Object.DestroyImmediate(board.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Local_Seat_Is_Always_Bottom_Of_The_Board()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var board = GameBoard3D.Create(null, config);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
board.SetVisible(true);
|
||||||
|
board.Render(BuildView(you: 1), new BoardRenderState());
|
||||||
|
|
||||||
|
var vanguard = ActiveCards().Single(card =>
|
||||||
|
card.Ref.Zone == BoardZone.Vanguard
|
||||||
|
&& card.Ref.Player == 1);
|
||||||
|
Assert.Less(vanguard.TargetPosition.z, 0f);
|
||||||
|
|
||||||
|
var opponent = ActiveCards().Single(card =>
|
||||||
|
card.Ref.Zone == BoardZone.Vanguard
|
||||||
|
&& card.Ref.Player == 0);
|
||||||
|
Assert.Greater(opponent.TargetPosition.z, 0f);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Object.DestroyImmediate(board.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Highlight_And_Selection_State_Is_Applied_To_Cards()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var board = GameBoard3D.Create(null, config);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
board.SetVisible(true);
|
||||||
|
var state = new BoardRenderState();
|
||||||
|
state.Highlights.Add(new BoardSlotKey(0, 3));
|
||||||
|
state.SelectedHandInstanceIds.Add(12);
|
||||||
|
board.Render(BuildView(you: 0), state);
|
||||||
|
|
||||||
|
var rested = ActiveCards().Single(card =>
|
||||||
|
card.Ref.Zone == BoardZone.RearGuard
|
||||||
|
&& card.Ref.Player == 0
|
||||||
|
&& card.Ref.Slot == 3);
|
||||||
|
Assert.IsTrue(rested.transform.Find("Highlight").gameObject.activeSelf);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Object.DestroyImmediate(board.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Card3D> ActiveCards() =>
|
||||||
|
Resources.FindObjectsOfTypeAll<Card3D>()
|
||||||
|
.Where(card => card.gameObject.activeInHierarchy)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static MatchView BuildView(int you)
|
||||||
|
{
|
||||||
|
CardView Card(string id, int instance, bool rested = false, bool faceUp = true) =>
|
||||||
|
new CardView
|
||||||
|
{
|
||||||
|
PrintingId = id,
|
||||||
|
InstanceId = instance,
|
||||||
|
Rested = rested,
|
||||||
|
FaceUp = faceUp,
|
||||||
|
Power = 8000,
|
||||||
|
BasePower = 8000,
|
||||||
|
Critical = 1,
|
||||||
|
BaseCritical = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
var self = new PlayerView
|
||||||
|
{
|
||||||
|
Index = you,
|
||||||
|
Name = "Self",
|
||||||
|
Vanguard = Card("BT01/001EN", 1),
|
||||||
|
RearGuards = new CardView[5],
|
||||||
|
Hand = new List<CardView>
|
||||||
|
{
|
||||||
|
Card("BT01/002EN", 11),
|
||||||
|
Card("BT01/003EN", 12),
|
||||||
|
},
|
||||||
|
Guardian = new List<CardView> { Card("BT01/004EN", 7) },
|
||||||
|
Soul = new List<CardView> { Card("BT01/005EN", 8) },
|
||||||
|
FaceUpDamage = new List<CardView> { Card("BT01/006EN", 9) },
|
||||||
|
FaceDownDamage = 2,
|
||||||
|
DeckCount = 40,
|
||||||
|
Drop = new List<CardView>(),
|
||||||
|
};
|
||||||
|
self.RearGuards[3] = Card("BT01/007EN", 5, rested: true);
|
||||||
|
|
||||||
|
var otherIndex = 1 - you;
|
||||||
|
var opponent = new PlayerView
|
||||||
|
{
|
||||||
|
Index = otherIndex,
|
||||||
|
Name = "Opponent",
|
||||||
|
Vanguard = Card("BT01/010EN", 21),
|
||||||
|
RearGuards = new CardView[5],
|
||||||
|
Hand = new List<CardView>(),
|
||||||
|
Guardian = new List<CardView>(),
|
||||||
|
Soul = new List<CardView>(),
|
||||||
|
FaceUpDamage = new List<CardView>(),
|
||||||
|
FaceDownDamage = 0,
|
||||||
|
DeckCount = 40,
|
||||||
|
Drop = new List<CardView>(),
|
||||||
|
};
|
||||||
|
opponent.RearGuards[4] = Card("BT01/011EN", 22, rested: true);
|
||||||
|
|
||||||
|
return new MatchView
|
||||||
|
{
|
||||||
|
MatchId = "board-test",
|
||||||
|
You = you,
|
||||||
|
ActivePlayer = you,
|
||||||
|
Phase = GamePhase.Main,
|
||||||
|
Players = new List<PlayerView> { self, opponent },
|
||||||
|
Clock = new ClockView(),
|
||||||
|
LegalIntents = new List<IntentPayload>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9caafc3153efd3241b024681536dc78a
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
using Vanguard.Board3D;
|
||||||
|
|
||||||
|
namespace Vanguard.Tests
|
||||||
|
{
|
||||||
|
public sealed class PlaymatLayoutTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Default_Playmat_Has_Six_Circles_Per_Side()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var layout = new PlaymatLayout(config);
|
||||||
|
|
||||||
|
for (var seat = 0; seat < 2; seat++)
|
||||||
|
{
|
||||||
|
var positions = Enumerable.Range(-1, PlaymatLayout.RearGuardCount + 1)
|
||||||
|
.Select(slot => layout.CirclePosition(seat, slot))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.AreEqual(6, positions.Count);
|
||||||
|
Assert.AreEqual(6, positions.Select(position => position.ToString()).Distinct().Count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Vanguard_Is_Center_Front_And_Rear_Center_Exists()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var layout = new PlaymatLayout(config);
|
||||||
|
|
||||||
|
var vanguard = layout.CirclePosition(0, PlaymatLayout.VanguardSlot);
|
||||||
|
var rearCenter = layout.CirclePosition(0, 1);
|
||||||
|
var leftFront = layout.CirclePosition(0, 3);
|
||||||
|
var rightFront = layout.CirclePosition(0, 4);
|
||||||
|
|
||||||
|
Assert.AreEqual(0f, vanguard.x, 0.0001f);
|
||||||
|
Assert.AreEqual(-config.frontRowZ, vanguard.z, 0.0001f);
|
||||||
|
Assert.AreEqual(0f, rearCenter.x, 0.0001f);
|
||||||
|
Assert.AreEqual(-config.backRowZ, rearCenter.z, 0.0001f);
|
||||||
|
Assert.Less(leftFront.x, 0f);
|
||||||
|
Assert.Greater(rightFront.x, 0f);
|
||||||
|
Assert.AreEqual(vanguard.z, leftFront.z, 0.0001f);
|
||||||
|
Assert.AreEqual(vanguard.z, rightFront.z, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Opponent_Half_Is_Mirrored()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var layout = new PlaymatLayout(config);
|
||||||
|
|
||||||
|
for (var slot = PlaymatLayout.VanguardSlot; slot < PlaymatLayout.RearGuardCount; slot++)
|
||||||
|
{
|
||||||
|
var self = layout.CirclePosition(0, slot);
|
||||||
|
var opponent = layout.CirclePosition(1, slot);
|
||||||
|
Assert.AreEqual(-self.x, opponent.x, 0.0001f);
|
||||||
|
Assert.AreEqual(-self.z, opponent.z, 0.0001f);
|
||||||
|
Assert.AreEqual(self.y, opponent.y, 0.0001f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Damage_And_Hand_Do_Not_Overlap_Field()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var layout = new PlaymatLayout(config);
|
||||||
|
|
||||||
|
var damage = Enumerable.Range(0, PlaymatLayout.DamageSlots)
|
||||||
|
.Select(index => layout.DamagePosition(0, index))
|
||||||
|
.ToList();
|
||||||
|
Assert.AreEqual(PlaymatLayout.DamageSlots,
|
||||||
|
damage.Select(position => position.ToString()).Distinct().Count());
|
||||||
|
|
||||||
|
var hand = Enumerable.Range(0, 5)
|
||||||
|
.Select(index => layout.GetHandPose(index, 5).Position)
|
||||||
|
.ToList();
|
||||||
|
Assert.AreEqual(5, hand.Select(position => position.ToString()).Distinct().Count());
|
||||||
|
Assert.IsTrue(hand.All(position => position.z < -4f));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Drop_Target_Finds_Own_And_Opponent_Circles()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var layout = new PlaymatLayout(config);
|
||||||
|
|
||||||
|
var nearPoint = layout.CirclePosition(0, 3);
|
||||||
|
Assert.IsTrue(layout.TryFindDropTarget(nearPoint, selfPlayer: 1, out var near));
|
||||||
|
Assert.AreEqual(BoardZone.RearGuard, near.Zone);
|
||||||
|
Assert.AreEqual(3, near.Slot);
|
||||||
|
Assert.AreEqual(1, near.Player);
|
||||||
|
|
||||||
|
var farPoint = layout.CirclePosition(1, -1);
|
||||||
|
Assert.IsTrue(layout.TryFindDropTarget(farPoint, selfPlayer: 1, out var far));
|
||||||
|
Assert.AreEqual(BoardZone.Vanguard, far.Zone);
|
||||||
|
Assert.AreEqual(-1, far.Slot);
|
||||||
|
Assert.AreEqual(0, far.Player);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Drop_Target_Recognizes_Shared_Guardian_Zone()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var layout = new PlaymatLayout(config);
|
||||||
|
|
||||||
|
Assert.IsTrue(layout.TryFindDropTarget(
|
||||||
|
new Vector3(0f, 0f, -0.45f),
|
||||||
|
selfPlayer: 0,
|
||||||
|
out var target));
|
||||||
|
Assert.AreEqual(BoardZone.Guardian, target.Zone);
|
||||||
|
Assert.AreEqual(0, target.Player);
|
||||||
|
|
||||||
|
Assert.IsFalse(layout.TryFindDropTarget(
|
||||||
|
new Vector3(0f, 0f, -7f),
|
||||||
|
selfPlayer: 0,
|
||||||
|
out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Move_Is_Limited_To_Own_Column()
|
||||||
|
{
|
||||||
|
Assert.IsTrue(BoardPlacementRules.CanMove(0, 3));
|
||||||
|
Assert.IsTrue(BoardPlacementRules.CanMove(3, 0));
|
||||||
|
Assert.IsTrue(BoardPlacementRules.CanMove(2, 4));
|
||||||
|
Assert.IsTrue(BoardPlacementRules.CanMove(4, 2));
|
||||||
|
Assert.IsFalse(BoardPlacementRules.CanMove(1, 0));
|
||||||
|
Assert.IsFalse(BoardPlacementRules.CanMove(0, 1));
|
||||||
|
Assert.IsFalse(BoardPlacementRules.CanMove(3, 4));
|
||||||
|
Assert.IsFalse(BoardPlacementRules.CanMove(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Call_Ride_And_Guard_Follow_Classic_Circles()
|
||||||
|
{
|
||||||
|
Assert.IsTrue(BoardPlacementRules.CanCall(0, occupied: false));
|
||||||
|
Assert.IsTrue(BoardPlacementRules.CanCall(4, occupied: false));
|
||||||
|
Assert.IsFalse(BoardPlacementRules.CanCall(0, occupied: true));
|
||||||
|
Assert.IsFalse(BoardPlacementRules.CanCall(5, occupied: false));
|
||||||
|
|
||||||
|
Assert.IsTrue(BoardPlacementRules.CanRide(BoardZone.Vanguard, -1));
|
||||||
|
Assert.IsFalse(BoardPlacementRules.CanRide(BoardZone.RearGuard, 0));
|
||||||
|
Assert.IsTrue(BoardPlacementRules.CanGuard(BoardZone.Guardian));
|
||||||
|
Assert.IsFalse(BoardPlacementRules.CanGuard(BoardZone.Damage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Call_Ride_Enter_Stand_And_Move_Preserves_State()
|
||||||
|
{
|
||||||
|
Assert.AreEqual(
|
||||||
|
CardState3D.FaceUpStand,
|
||||||
|
BoardPlacementRules.EnterFromHand(CircleAttribute.Vanguard));
|
||||||
|
Assert.AreEqual(
|
||||||
|
CardState3D.FaceUpStand,
|
||||||
|
BoardPlacementRules.EnterFromHand(CircleAttribute.RearGuard));
|
||||||
|
|
||||||
|
var rested = CardState3D.FaceUpRest;
|
||||||
|
Assert.AreEqual(rested, BoardPlacementRules.PreserveOnMove(rested));
|
||||||
|
Assert.IsTrue(BoardPlacementRules.PreserveOnMove(rested).Rested);
|
||||||
|
Assert.IsTrue(BoardPlacementRules.PreserveOnMove(rested).FaceUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 961f459713ed4e7469a79379d1d9afaf
|
||||||
|
|
@ -832,3 +832,36 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
клиенту открытыми; в круге защиты дополнительно рисуется ряд карт.
|
клиенту открытыми; в круге защиты дополнительно рисуется ряд карт.
|
||||||
- Classic-режим не включает Stride/Вознесение, Legacy, Gift, Front-триггер
|
- Classic-режим не включает Stride/Вознесение, Legacy, Gift, Front-триггер
|
||||||
и Grade support.
|
и Grade support.
|
||||||
|
|
||||||
|
## 2026-10-22 — 3D Playmat
|
||||||
|
|
||||||
|
- Боевое поле переведено в 3D: `Runtime/Board3D` (`Config`, `Data`,
|
||||||
|
`Logic`, `View`), HUD/кнопки остаются uGUI.
|
||||||
|
- Раскладка: локальный игрок всегда внизу; передний ряд `RC–V–RC`, задний
|
||||||
|
`RC–RC–RC`; у противника зеркально. Между половинами — общий круг защиты
|
||||||
|
(карты обоих игроков и текущий Guard). Душа — стопка под V, колода и дроп
|
||||||
|
сбоку, урон — две колонки по три карты на боку.
|
||||||
|
- `PlaymatLayoutConfig` (ScriptableObject): размеры карт, позиции рядов,
|
||||||
|
высоты, рука, камера, палитра. Ассет:
|
||||||
|
`Runtime/Board3D/Config/PlaymatLayoutConfig.asset`.
|
||||||
|
- `PlaymatLayout` — чистая геометрия и поиск цели броска; `BoardPlacementRules`
|
||||||
|
— Ride только на V, Call только на пустой RC, Move только в своей колонке
|
||||||
|
(0↔3, 2↔4, занятый круг меняется местами), Guard — в общий круг.
|
||||||
|
- `Card3D` — тонкая коробка: отдельные лицо/рубашка/торец, коллайдер, арт из
|
||||||
|
`CardImageService`, переворот вокруг X, Rest вокруг Y, плавный снап.
|
||||||
|
Карты пулятся (`GameBoard3D`), материалы общие, instancing включён.
|
||||||
|
- `GameBoard3D` строит стол, круги, зоны, стопки и веер руки; raycast-клики,
|
||||||
|
drag&drop на плоскость стола с подъёмом, подсветкой допустимых кругов и
|
||||||
|
индикатором точки броска. Ошибка интента возвращает карту на место.
|
||||||
|
- Камера: `BoardCameraRig` — ПКМ орбита (±32°), СКМ панорама, колесо зум
|
||||||
|
(9.5–16); ограничения в конфиге.
|
||||||
|
- Душа: клик по стопке или кнопка «Душа: n» в верхней панели открывает
|
||||||
|
оверлей со списком карт.
|
||||||
|
- Песочница: `Assets/Vanguard/Scenes/GameBoard3D.unity` +
|
||||||
|
`PlaymatSandbox`; собирается `Vanguard → Rebuild Playmat Sandbox`,
|
||||||
|
проверяется `SceneSmoke.RunPlaymat`.
|
||||||
|
- Проверки: EditMode **17/17** (геометрия, зеркальность, правила, рендер
|
||||||
|
3D-карт, пул), `SCENE_SMOKE success=True`,
|
||||||
|
`PLAYMAT_SMOKE success=True cards=20`.
|
||||||
|
- Ограничения: retire/ghost-анимации старого 2D-поля пока не перенесены;
|
||||||
|
модели карт/стола — процедурные примитивы.
|
||||||
|
|
|
||||||
|
|
@ -61,13 +61,16 @@
|
||||||
- Тренировка с ботом: кнопка «С ботом» в редакторе колоды; сервер
|
- Тренировка с ботом: кнопка «С ботом» в редакторе колоды; сервер
|
||||||
создаёт casual-матч против случайного бота (`POST /api/game/practice`),
|
создаёт casual-матч против случайного бота (`POST /api/game/practice`),
|
||||||
без истории, MMR и наград.
|
без истории, MMR и наград.
|
||||||
- Боевой экран: реальный арт на всю карту, рамка по клану, имя, ранг,
|
- Боевой экран: поле, рука и стопки — **3D** (`GameBoard3D`), HUD/кнопки
|
||||||
триггер, сила/крит и рубашка; fallback — процедурная плашка.
|
остаются uGUI. Карты — тонкие 3D-объекты с артом, рубашкой, торцом и
|
||||||
Rest поворачивает карту на бок. Карты в кругах V/R и в круге защиты
|
коллайдером. Rest — поворот вокруг Y, закрытость — переворот вокруг X.
|
||||||
показываются открытыми.
|
Драг-н-дроп на плоскость стола с подсветкой целей и плавным снапом.
|
||||||
- Поле: у каждого игрока два ряда `R V R` / `R R R`, слева зона урона
|
- Поле: у каждого игрока два ряда `R V R` / `R R R` (V + 5 RC), у
|
||||||
(+soul), справа колода и ниже дроп; у противника зеркально; между
|
противника зеркально; между полями общий круг защиты; душа — стопка под
|
||||||
полями — зона защиты со щитом и проверками.
|
V; колода, дроп и урон — 3D-стопки/карты. Локальный игрок всегда снизу,
|
||||||
|
даже если он за вторым местом. ПКМ — орбита, СКМ — панорама, колесо —
|
||||||
|
зум. Кнопка «Душа: n» в верхней панели и клик по стопке души открывают
|
||||||
|
список карт души.
|
||||||
- Зона урона: карты лежат открытыми и на боку; после Counter Blast
|
- Зона урона: карты лежат открытыми и на боку; после Counter Blast
|
||||||
выбранная карта переворачивается рубашкой. Heal выбирает карту урона;
|
выбранная карта переворачивается рубашкой. Heal выбирает карту урона;
|
||||||
Counter Blast — открытую карту до активации способности.
|
Counter Blast — открытую карту до активации способности.
|
||||||
|
|
@ -75,8 +78,8 @@
|
||||||
колоды, добираются новые, затем shuffle. Клик по руке выбирает карты.
|
колоды, добираются новые, затем shuffle. Клик по руке выбирает карты.
|
||||||
- Анимации: event-фидбек (атака, guard, drive, урон, retire, heal) поверх
|
- Анимации: event-фидбек (атака, guard, drive, урон, retire, heal) поверх
|
||||||
стола, не блокирует ввод; очередь событий + кнопка «Пропустить анимации».
|
стола, не блокирует ввод; очередь событий + кнопка «Пропустить анимации».
|
||||||
Карты въезжают из руки/колоды при call/ride, атакующий и цель пульсируют,
|
3D-карты плавно снапятся, поворачиваются в Rest и переворачиваются;
|
||||||
ушедшие в дроп карты летят призраком и растворяются.
|
подсветка целей и точки броска обновляются на каждом снимке.
|
||||||
- Реальные арты: `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`.
|
||||||
|
|
@ -87,8 +90,10 @@
|
||||||
В первый ход атака запрещена; первый игрок определяется сидом.
|
В первый ход атака запрещена; первый игрок определяется сидом.
|
||||||
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
||||||
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
||||||
- Не готово: 3D-модели, звук, настройки (язык уже есть), DOTween,
|
- Не готово: 3D-модели карт/стола (пока процедурные примитивы и арты),
|
||||||
drag&drop и live-валидация декбилдера.
|
звук, настройки (язык уже есть), DOTween, drag&drop и live-валидация
|
||||||
|
декбилдера. Камера боя — ограниченная орбита; анимации retire/ghost для
|
||||||
|
3D-карт ещё не перенесены.
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
|
|
@ -118,11 +123,17 @@ curl http://127.0.0.1:18080/health
|
||||||
& 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' `
|
& 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' `
|
||||||
-batchmode -nographics -projectPath client `
|
-batchmode -nographics -projectPath client `
|
||||||
-executeMethod Vanguard.Editor.SceneSmoke.Run
|
-executeMethod Vanguard.Editor.SceneSmoke.Run
|
||||||
|
|
||||||
|
# 3D playmat sandbox
|
||||||
|
& 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' `
|
||||||
|
-batchmode -nographics -projectPath client `
|
||||||
|
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||||
```
|
```
|
||||||
|
|
||||||
Клиентские проверки: **4/4** EditMode (сборка всех экранов, импорт арта,
|
Клиентские проверки: **17/17** EditMode (сборка всех экранов, импорт арта,
|
||||||
перевод статических/динамических строк, live-переключение языка, рендер
|
перевод статических/динамических строк, live-переключение языка, рендер
|
||||||
доски с уроном и отдыхающей картой), `SCENE_SMOKE success=True`.
|
3D-доски с уроном и отдыхающей картой, геометрия поля и правила
|
||||||
|
перемещения), `SCENE_SMOKE success=True`, `PLAYMAT_SMOKE success=True cards=20`.
|
||||||
|
|
||||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||||
ломает стандартный PlayMode test runner. `SceneBuilder` пересобирает сцены:
|
ломает стандартный PlayMode test runner. `SceneBuilder` пересобирает сцены:
|
||||||
|
|
@ -148,6 +159,16 @@ client/Assets/Vanguard/Runtime/UI/
|
||||||
LoginView.cs MetaView.cs OnboardingView.cs
|
LoginView.cs MetaView.cs OnboardingView.cs
|
||||||
DeckBuilderView.cs QueueView.cs BattleView.cs
|
DeckBuilderView.cs QueueView.cs BattleView.cs
|
||||||
UiKit.cs Localization.cs
|
UiKit.cs Localization.cs
|
||||||
|
client/Assets/Vanguard/Runtime/Board3D/
|
||||||
|
Config/PlaymatLayoutConfig.cs ScriptableObject-раскладка
|
||||||
|
Data/BoardTypes.cs зоны, круги, CardState3D, refs
|
||||||
|
Logic/PlaymatLayout.cs геометрия, рука, поиск цели броска
|
||||||
|
Logic/BoardPlacementRules.cs Ride/Call/Move/Guard
|
||||||
|
View/GameBoard3D.cs стол, круги, пул карт, drag&drop
|
||||||
|
View/Card3D.cs карта: лицо/рубашка/торец, Rest, флип
|
||||||
|
View/BoardCameraRig.cs орбита/панорама/зум
|
||||||
|
PlaymatSandbox.cs демо без сервера
|
||||||
|
client/Assets/Vanguard/Scenes/GameBoard3D.unity песочница поля
|
||||||
client/Assets/Vanguard/Runtime/Battle/GameConnection.cs
|
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
|
||||||
|
|
@ -171,7 +192,7 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
|
||||||
## Следующие шаги
|
## Следующие шаги
|
||||||
|
|
||||||
1. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты.
|
1. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты.
|
||||||
2. 3D-наполнение: модели/арт, prefab-экраны, камера.
|
2. 3D-наполнение: модели карт/стола, prefab-экраны, анимации retire/ghost.
|
||||||
3. Звук и настройки, DOTween.
|
3. Звук и настройки, DOTween.
|
||||||
4. Лаунчер WPF.
|
4. Лаунчер WPF.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,10 @@
|
||||||
Сцены `Login`, `Lobby`, `DeckBuilder`, `Battle` созданы;
|
Сцены `Login`, `Lobby`, `DeckBuilder`, `Battle` созданы;
|
||||||
`VanguardApp` и общие сервисы персистентны, `SceneFlow` загружает сцены.
|
`VanguardApp` и общие сервисы персистентны, `SceneFlow` загружает сцены.
|
||||||
Сейчас 3D-окружение собрано из примитивов, UI — code-generated uGUI.
|
Сейчас 3D-окружение собрано из примитивов, UI — code-generated uGUI.
|
||||||
Дальше: арт/модели, prefab-экраны, анимации и наполнение сцен.
|
Боевое поле уже 3D: `GameBoard3D` строит стол, круги, руку и стопки,
|
||||||
|
поддерживает drag&drop, Rest/фейс-даун и ограниченную камеру. Песочница —
|
||||||
|
`Assets/Vanguard/Scenes/GameBoard3D.unity`.
|
||||||
|
Дальше: 3D-модели карт/стола, prefab-экраны, анимации и наполнение сцен.
|
||||||
|
|
||||||
Уже перенесено:
|
Уже перенесено:
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue