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.SceneManagement;
|
||||
using Vanguard.App;
|
||||
using Vanguard.Board3D;
|
||||
|
||||
namespace Vanguard.Editor
|
||||
{
|
||||
|
|
@ -17,6 +18,9 @@ namespace Vanguard.Editor
|
|||
{
|
||||
private const string SceneDir = "Assets/Vanguard/Scenes";
|
||||
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")]
|
||||
public static void BuildAll()
|
||||
|
|
@ -31,6 +35,7 @@ namespace Vanguard.Editor
|
|||
Create<LobbySceneController>("Lobby");
|
||||
Create<DeckBuilderSceneController>("DeckBuilder");
|
||||
Create<BattleSceneController>("Battle");
|
||||
EnsurePlaymatConfig();
|
||||
|
||||
var scenes = new List<EditorBuildSettingsScene>();
|
||||
Add(scenes, BootPath);
|
||||
|
|
@ -46,6 +51,66 @@ namespace Vanguard.Editor
|
|||
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
|
||||
{
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,40 @@ namespace Vanguard.Editor
|
|||
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()
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
|
|
|
|||
|
|
@ -51,15 +51,15 @@ namespace Vanguard.App
|
|||
|
||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
camera.backgroundColor = palette.Background;
|
||||
camera.fieldOfView = 52f;
|
||||
camera.fieldOfView = theme == SceneTheme.Battle ? 50f : 52f;
|
||||
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),
|
||||
_ => new Vector3(0f, 3.2f, -10f),
|
||||
};
|
||||
camera.transform.rotation = theme == SceneTheme.Battle
|
||||
? Quaternion.Euler(28f, 0f, 0f)
|
||||
? Quaternion.Euler(34.4f, 0f, 0f)
|
||||
: Quaternion.Euler(8f, 0f, 0f);
|
||||
}
|
||||
|
||||
|
|
@ -149,14 +149,11 @@ namespace Vanguard.App
|
|||
break;
|
||||
|
||||
case SceneTheme.Battle:
|
||||
Cube(root, "Battle Table", new Vector3(0f, 0.2f, 1.5f), new Vector3(13f, 0.4f, 7f), palette.Structure);
|
||||
Cube(root, "Center Line", new Vector3(0f, 0.44f, 1.5f), new Vector3(13f, 0.04f, 0.12f), palette.Accent, 1.2f);
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
var x = (i - 2) * 2.2f;
|
||||
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);
|
||||
}
|
||||
// Сам стол и круги строит GameBoard3D. Здесь только фон.
|
||||
Cube(root, "Back Wall", new Vector3(0f, 4.2f, 9.5f), new Vector3(24f, 8.4f, 0.5f), palette.Structure);
|
||||
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);
|
||||
Cube(root, "Back Light", new Vector3(0f, 5.4f, 9f), new Vector3(6.5f, 0.16f, 0.8f), palette.Accent, 1.8f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ namespace Vanguard.App
|
|||
public const string Lobby = "Lobby";
|
||||
public const string DeckBuilder = "DeckBuilder";
|
||||
public const string Battle = "Battle";
|
||||
public const string PlaymatSandbox = "GameBoard3D";
|
||||
|
||||
public DeckDto PendingQueueDeck { get; private set; }
|
||||
public MatchConnectionInfo PendingMatch { get; private set; }
|
||||
|
|
|
|||
|
|
@ -56,9 +56,13 @@ namespace Vanguard.App
|
|||
|
||||
private void Start()
|
||||
{
|
||||
if (Flow != null)
|
||||
if (Flow != null
|
||||
&& UnityEngine.SceneManagement.SceneManager.GetActiveScene().name
|
||||
!= SceneFlow.PlaymatSandbox)
|
||||
{
|
||||
_ = Flow.LoadLoginAsync();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return;
|
||||
|
||||
target.sprite = fallback;
|
||||
var sprite = Resolve(imageKey, fallback);
|
||||
target.sprite = sprite;
|
||||
target.color = Color.white;
|
||||
target.preserveAspect = false;
|
||||
}
|
||||
|
||||
/// <summary>Спрайт реального арта или процедурный fallback.</summary>
|
||||
public static Sprite Resolve(string imageKey, Sprite fallback)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imageKey))
|
||||
return;
|
||||
return fallback;
|
||||
|
||||
if (Cache.TryGetValue(imageKey, out var cached))
|
||||
{
|
||||
if (cached != null)
|
||||
target.sprite = cached;
|
||||
return;
|
||||
}
|
||||
return cached != null ? cached : fallback;
|
||||
|
||||
var texture = Resources.Load<Texture2D>(ResourceFolder + imageKey);
|
||||
if (texture == null)
|
||||
{
|
||||
Cache[imageKey] = null;
|
||||
return;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
var sprite = Sprite.Create(
|
||||
|
|
@ -48,7 +49,7 @@ namespace Vanguard.UI
|
|||
new Vector2(0.5f, 0.5f),
|
||||
100f);
|
||||
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.App;
|
||||
using Vanguard.Auth;
|
||||
using Vanguard.Board3D;
|
||||
using Vanguard.Meta;
|
||||
using Vanguard.Shared.Protocol;
|
||||
using Vanguard.State;
|
||||
|
|
@ -120,7 +121,7 @@ namespace Vanguard.Tests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void Battle_Renders_Damage_And_Rested_Cards()
|
||||
public void Battle_Renders_3D_Damage_And_Rested_Cards()
|
||||
{
|
||||
var config = new AppConfig();
|
||||
config.Load();
|
||||
|
|
@ -192,6 +193,10 @@ namespace Vanguard.Tests
|
|||
LegalIntents = new List<IntentPayload>(),
|
||||
};
|
||||
|
||||
Resources.FindObjectsOfTypeAll<GameBoard3D>()
|
||||
.FirstOrDefault()
|
||||
?.SetVisible(true);
|
||||
|
||||
typeof(BattleView)
|
||||
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.SetValue(battle, match);
|
||||
|
|
@ -199,21 +204,30 @@ namespace Vanguard.Tests
|
|||
.GetMethod("Render", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.Invoke(battle, null);
|
||||
|
||||
Assert.IsTrue(Exists("DamageZone"));
|
||||
Assert.IsTrue(Exists("Damage_0"));
|
||||
Assert.IsTrue(Exists("Damage_1"));
|
||||
Assert.IsTrue(Exists("Damage_2"));
|
||||
Assert.IsTrue(Exists("Guard_7"));
|
||||
Assert.IsTrue(Exists("Card_R4"));
|
||||
var hasSidewaysCard = Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Where(go => go.name == "Card_R4")
|
||||
.Any(go =>
|
||||
{
|
||||
var angle = go.transform.localEulerAngles.z;
|
||||
return Mathf.Abs(Mathf.DeltaAngle(angle, 90f)) < 1f
|
||||
|| Mathf.Abs(Mathf.DeltaAngle(angle, -90f)) < 1f;
|
||||
});
|
||||
Assert.IsTrue(hasSidewaysCard, "Rested card was not turned sideways");
|
||||
Assert.IsTrue(Exists("GameBoard3D"));
|
||||
|
||||
var cards = Resources.FindObjectsOfTypeAll<Card3D>()
|
||||
.Where(card => card.gameObject.activeInHierarchy)
|
||||
.ToList();
|
||||
Assert.GreaterOrEqual(cards.Count, 3);
|
||||
|
||||
var rested = cards.FirstOrDefault(card =>
|
||||
card.Ref.Zone == BoardZone.RearGuard
|
||||
&& card.Ref.Player == 0
|
||||
&& card.Ref.Slot == 3);
|
||||
Assert.IsNotNull(rested, "Rested rear-guard was not rendered in 3D");
|
||||
Assert.IsTrue(rested.IsRested);
|
||||
Assert.AreEqual(90f, Mathf.Abs(Mathf.DeltaAngle(rested.TargetYaw, 0f)), 0.1f);
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
@ -223,6 +237,12 @@ namespace Vanguard.Tests
|
|||
|
||||
private static void Cleanup()
|
||||
{
|
||||
foreach (var board in Resources.FindObjectsOfTypeAll<GameBoard3D>())
|
||||
{
|
||||
if (board != null)
|
||||
Object.DestroyImmediate(board.gameObject);
|
||||
}
|
||||
|
||||
var canvas = GameObject.Find("Test Canvas");
|
||||
if (canvas != null)
|
||||
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-триггер
|
||||
и 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`),
|
||||
без истории, MMR и наград.
|
||||
- Боевой экран: реальный арт на всю карту, рамка по клану, имя, ранг,
|
||||
триггер, сила/крит и рубашка; fallback — процедурная плашка.
|
||||
Rest поворачивает карту на бок. Карты в кругах V/R и в круге защиты
|
||||
показываются открытыми.
|
||||
- Поле: у каждого игрока два ряда `R V R` / `R R R`, слева зона урона
|
||||
(+soul), справа колода и ниже дроп; у противника зеркально; между
|
||||
полями — зона защиты со щитом и проверками.
|
||||
- Боевой экран: поле, рука и стопки — **3D** (`GameBoard3D`), HUD/кнопки
|
||||
остаются uGUI. Карты — тонкие 3D-объекты с артом, рубашкой, торцом и
|
||||
коллайдером. Rest — поворот вокруг Y, закрытость — переворот вокруг X.
|
||||
Драг-н-дроп на плоскость стола с подсветкой целей и плавным снапом.
|
||||
- Поле: у каждого игрока два ряда `R V R` / `R R R` (V + 5 RC), у
|
||||
противника зеркально; между полями общий круг защиты; душа — стопка под
|
||||
V; колода, дроп и урон — 3D-стопки/карты. Локальный игрок всегда снизу,
|
||||
даже если он за вторым местом. ПКМ — орбита, СКМ — панорама, колесо —
|
||||
зум. Кнопка «Душа: n» в верхней панели и клик по стопке души открывают
|
||||
список карт души.
|
||||
- Зона урона: карты лежат открытыми и на боку; после Counter Blast
|
||||
выбранная карта переворачивается рубашкой. Heal выбирает карту урона;
|
||||
Counter Blast — открытую карту до активации способности.
|
||||
|
|
@ -75,8 +78,8 @@
|
|||
колоды, добираются новые, затем shuffle. Клик по руке выбирает карты.
|
||||
- Анимации: event-фидбек (атака, guard, drive, урон, retire, heal) поверх
|
||||
стола, не блокирует ввод; очередь событий + кнопка «Пропустить анимации».
|
||||
Карты въезжают из руки/колоды при call/ride, атакующий и цель пульсируют,
|
||||
ушедшие в дроп карты летят призраком и растворяются.
|
||||
3D-карты плавно снапятся, поворачиваются в Rest и переворачиваются;
|
||||
подсветка целей и точки броска обновляются на каждом снимке.
|
||||
- Реальные арты: `imageKey` из `/api/cards` + `CardImageService` ищет
|
||||
`Resources/CardArt/{imageKey}`. Скачаны все 124 арта. Импорт:
|
||||
`tools/import_card_art.py`.
|
||||
|
|
@ -87,8 +90,10 @@
|
|||
В первый ход атака запрещена; первый игрок определяется сидом.
|
||||
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
||||
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
||||
- Не готово: 3D-модели, звук, настройки (язык уже есть), DOTween,
|
||||
drag&drop и live-валидация декбилдера.
|
||||
- Не готово: 3D-модели карт/стола (пока процедурные примитивы и арты),
|
||||
звук, настройки (язык уже есть), 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' `
|
||||
-batchmode -nographics -projectPath client `
|
||||
-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-переключение языка, рендер
|
||||
доски с уроном и отдыхающей картой), `SCENE_SMOKE success=True`.
|
||||
3D-доски с уроном и отдыхающей картой, геометрия поля и правила
|
||||
перемещения), `SCENE_SMOKE success=True`, `PLAYMAT_SMOKE success=True cards=20`.
|
||||
|
||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||
ломает стандартный PlayMode test runner. `SceneBuilder` пересобирает сцены:
|
||||
|
|
@ -148,6 +159,16 @@ client/Assets/Vanguard/Runtime/UI/
|
|||
LoginView.cs MetaView.cs OnboardingView.cs
|
||||
DeckBuilderView.cs QueueView.cs BattleView.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/Api/ApiClient.cs + ApiContracts.cs
|
||||
client/Assets/Vanguard/Editor/SceneBuilder.cs + SceneSmoke.cs
|
||||
|
|
@ -171,7 +192,7 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
|
|||
## Следующие шаги
|
||||
|
||||
1. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты.
|
||||
2. 3D-наполнение: модели/арт, prefab-экраны, камера.
|
||||
2. 3D-наполнение: модели карт/стола, prefab-экраны, анимации retire/ghost.
|
||||
3. Звук и настройки, DOTween.
|
||||
4. Лаунчер WPF.
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@
|
|||
Сцены `Login`, `Lobby`, `DeckBuilder`, `Battle` созданы;
|
||||
`VanguardApp` и общие сервисы персистентны, `SceneFlow` загружает сцены.
|
||||
Сейчас 3D-окружение собрано из примитивов, UI — code-generated uGUI.
|
||||
Дальше: арт/модели, prefab-экраны, анимации и наполнение сцен.
|
||||
Боевое поле уже 3D: `GameBoard3D` строит стол, круги, руку и стопки,
|
||||
поддерживает drag&drop, Rest/фейс-даун и ограниченную камеру. Песочница —
|
||||
`Assets/Vanguard/Scenes/GameBoard3D.unity`.
|
||||
Дальше: 3D-модели карт/стола, prefab-экраны, анимации и наполнение сцен.
|
||||
|
||||
Уже перенесено:
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue