initial commit

This commit is contained in:
MrTorex committed 2026-10-27 21:43:24 +03:00
commit f8db72668a
162 files changed
+75914

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class CardContainer : MonoBehaviour
{
public ContainerType type;
public RectTransform cardHolder;
public List<CardView> Cards { get; private set; } = new List<CardView>();
private void Awake()
{
if (cardHolder == null)
cardHolder = GetComponent<RectTransform>();
}
public void AddCardInstant(CardView card)
{
Cards.Add(card);
card.CurrentContainer = this;
card.transform.SetParent(cardHolder, false);
card.transform.localPosition = Vector3.zero;
card.SetFaceInstant(type != ContainerType.Deck);
}
public async UniTask AddCardAnimatedAsync(CardView card, int siblingIndex = -1, CancellationToken ct = default)
{
if (!Cards.Contains(card))
Cards.Add(card);
var shouldBeFaceUp = type != ContainerType.Deck;
var moveTask = card.AnimateToTargetAsync(this, siblingIndex, 0.3f, ct);
var flipTask = card.FlipAsync(shouldBeFaceUp, 0.25f, ct);
await UniTask.WhenAll(moveTask, flipTask);
SoundManager.Instance?.PlayDrop();
SyncListOrderWithHierarchy();
}
public void RemoveCardListReference(CardView card)
{
Cards.Remove(card);
}
public void SyncListOrderWithHierarchy()
{
Cards.Sort((a, b) => a.transform.GetSiblingIndex().CompareTo(b.transform.GetSiblingIndex()));
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5fba2adf289d437a822c075fb842c7e6
timeCreated: 1793123393
+72
View File
@@ -0,0 +1,72 @@
using System;
using UnityEngine;
public enum CardSuit
{
Fire = 0,
Earth = 1,
Air = 2,
Water = 3
}
public enum CardRank
{
Servant = 1,
Knight = 2,
Lord = 3
}
public enum ContainerType
{
Deck,
Hand,
Board,
Discard
}
[Serializable]
public struct CardModel
{
public CardSuit suit;
public CardRank rank;
public CardModel(CardSuit suit, CardRank rank)
{
this.suit = suit;
this.rank = rank;
}
public string GetTitle()
{
var rankName = rank switch
{
CardRank.Servant => "Слуга",
CardRank.Knight => "Рыцарь",
CardRank.Lord => "Владыка",
_ => ""
};
var suitName = suit switch
{
CardSuit.Fire => "Огня",
CardSuit.Earth => "Земли",
CardSuit.Air => "Воздуха",
CardSuit.Water => "Воды",
_ => ""
};
return $"{rankName}\n{suitName}\n({(int)rank})";
}
public Color GetSuitColor()
{
return suit switch
{
CardSuit.Fire => new Color(0.9f, 0.3f, 0.2f),
CardSuit.Earth => new Color(0.4f, 0.7f, 0.3f),
CardSuit.Air => new Color(0.8f, 0.9f, 0.95f),
CardSuit.Water => new Color(0.2f, 0.5f, 0.9f),
_ => Color.white
};
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ebcaabfef5174a14bf2f78765f870ee0
timeCreated: 1793123306
+266
View File
@@ -0,0 +1,266 @@
using System.Threading;
using Cysharp.Threading.Tasks;
using LitMotion;
using LitMotion.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
[RequireComponent(typeof(CanvasGroup))]
public class CardView : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
{
[Header("Visual Elements")]
[SerializeField] private GameObject frontFace;
[SerializeField] private GameObject backFace;
[SerializeField] private Image frontBackground;
[SerializeField] private TMP_Text titleText;
public CardModel Model { get; private set; }
public CardContainer CurrentContainer { get; set; }
public bool IsFaceUp { get; private set; } = true;
public bool IsInteractable { get; set; } = true;
private RectTransform _rectTransform;
private Canvas _rootCanvas;
private CanvasGroup _canvasGroup;
private GameObject _placeholder;
private CardContainer _originContainer;
private MotionHandle _moveMotion;
private MotionHandle _scaleMotion;
private MotionHandle _rotMotion;
private void Awake()
{
_rectTransform = GetComponent<RectTransform>();
_canvasGroup = GetComponent<CanvasGroup>();
_rootCanvas = GetComponentInParent<Canvas>();
}
public void Initialize(CardModel model, bool faceUp)
{
Model = model;
titleText.text = model.GetTitle();
frontBackground.color = model.GetSuitColor();
SetFaceInstant(faceUp);
}
public void SetFaceInstant(bool faceUp)
{
IsFaceUp = faceUp;
frontFace.SetActive(faceUp);
backFace.SetActive(!faceUp);
_rectTransform.localEulerAngles = Vector3.zero;
}
public async UniTask FlipAsync(bool faceUp, float duration = 0.25f, CancellationToken ct = default)
{
if (IsFaceUp == faceUp) return;
IsFaceUp = faceUp;
CancelMotion(ref _rotMotion);
SoundManager.Instance?.PlayFlip();
_rotMotion = LMotion.Create(0f, 90f, duration * 0.5f)
.WithEase(Ease.InQuad)
.BindToLocalEulerAnglesY(_rectTransform);
await _rotMotion.ToUniTask(ct);
frontFace.SetActive(faceUp);
backFace.SetActive(!faceUp);
_rotMotion = LMotion.Create(90f, 0f, duration * 0.5f)
.WithEase(Ease.OutQuad)
.BindToLocalEulerAnglesY(_rectTransform);
await _rotMotion.ToUniTask(ct);
}
public async UniTask AnimateToTargetAsync(CardContainer targetContainer, int siblingIndex = -1, float duration = 0.3f, CancellationToken ct = default)
{
CancelMotion(ref _moveMotion);
CancelMotion(ref _scaleMotion);
IsInteractable = false;
_canvasGroup.blocksRaycasts = false;
var tempSlot = new GameObject("TempSlot", typeof(RectTransform), typeof(LayoutElement));
var tempRect = tempSlot.GetComponent<RectTransform>();
tempRect.SetParent(targetContainer.cardHolder, false);
tempRect.sizeDelta = _rectTransform.sizeDelta;
if (siblingIndex >= 0 && siblingIndex <= targetContainer.cardHolder.childCount)
tempRect.SetSiblingIndex(siblingIndex);
LayoutRebuilder.ForceRebuildLayoutImmediate(targetContainer.cardHolder);
var targetWorldPos = tempRect.position;
transform.SetParent(_rootCanvas.transform, true);
_scaleMotion = LMotion.Create(_rectTransform.localScale, Vector3.one, duration)
.WithEase(Ease.OutQuad)
.BindToLocalScale(_rectTransform);
_moveMotion = LMotion.Create(_rectTransform.position, targetWorldPos, duration)
.WithEase(Ease.OutCubic)
.BindToPosition(_rectTransform);
await _moveMotion.ToUniTask(ct);
var finalIndex = tempRect.GetSiblingIndex();
Destroy(tempSlot);
transform.SetParent(targetContainer.cardHolder, false);
transform.SetSiblingIndex(finalIndex);
_rectTransform.localPosition = Vector3.zero;
_rectTransform.localScale = Vector3.one;
CurrentContainer = targetContainer;
_canvasGroup.blocksRaycasts = true;
IsInteractable = true;
}
public void OnPointerEnter(PointerEventData eventData)
{
if (!IsInteractable || eventData.dragging) return;
CancelMotion(ref _scaleMotion);
_scaleMotion = LMotion.Create(_rectTransform.localScale, Vector3.one * 1.08f, 0.15f)
.WithEase(Ease.OutQuad)
.BindToLocalScale(_rectTransform);
}
public void OnPointerExit(PointerEventData eventData)
{
if (!IsInteractable || eventData.dragging) return;
CancelMotion(ref _scaleMotion);
_scaleMotion = LMotion.Create(_rectTransform.localScale, Vector3.one, 0.15f)
.WithEase(Ease.OutQuad)
.BindToLocalScale(_rectTransform);
}
public void OnBeginDrag(PointerEventData eventData)
{
if (!IsInteractable || !GameManager.Instance.CanStartDrag(this))
{
eventData.pointerDrag = null;
return;
}
CancelMotion(ref _moveMotion);
CancelMotion(ref _scaleMotion);
SoundManager.Instance?.PlayDraw();
_originContainer = CurrentContainer;
_originContainer.RemoveCardListReference(this);
_placeholder = new GameObject("Placeholder", typeof(RectTransform), typeof(LayoutElement));
var rt = _placeholder.GetComponent<RectTransform>();
rt.SetParent(_originContainer.cardHolder, false);
rt.sizeDelta = _rectTransform.sizeDelta;
rt.SetSiblingIndex(transform.GetSiblingIndex());
var le = _placeholder.GetComponent<LayoutElement>();
le.preferredWidth = _rectTransform.rect.width;
le.preferredHeight = _rectTransform.rect.height;
transform.SetParent(_rootCanvas.transform, true);
_canvasGroup.blocksRaycasts = false;
_scaleMotion = LMotion.Create(_rectTransform.localScale, Vector3.one * 1.15f, 0.15f)
.WithEase(Ease.OutBack)
.BindToLocalScale(_rectTransform);
}
public void OnDrag(PointerEventData eventData)
{
if (!IsInteractable) return;
if (RectTransformUtility.ScreenPointToWorldPointInRectangle(
_rootCanvas.GetComponent<RectTransform>(),
eventData.position,
eventData.pressEventCamera,
out var globalMousePos))
{
_rectTransform.position = globalMousePos;
}
if (_originContainer != null && _originContainer.type == ContainerType.Hand && _placeholder != null)
{
UpdatePlaceholderPositionInHand();
}
}
private void UpdatePlaceholderPositionInHand()
{
Transform holder = _originContainer.cardHolder;
var newSiblingIndex = holder.childCount;
for (var i = 0; i < holder.childCount; i++)
{
var child = holder.GetChild(i);
if (child == _placeholder.transform) continue;
if (_rectTransform.position.x < child.position.x)
{
newSiblingIndex = i;
if (_placeholder.transform.GetSiblingIndex() < newSiblingIndex)
newSiblingIndex--;
break;
}
}
_placeholder.transform.SetSiblingIndex(newSiblingIndex);
}
public void OnEndDrag(PointerEventData eventData)
{
if (_placeholder == null) return;
var returnIndex = _placeholder.transform.GetSiblingIndex();
Destroy(_placeholder);
CardContainer targetContainer = null;
if (eventData.pointerEnter != null)
{
targetContainer = eventData.pointerEnter.GetComponentInParent<CardContainer>();
}
GameManager.Instance.HandleCardDropAsync(this, _originContainer, targetContainer, returnIndex).Forget();
}
public void OnPointerClick(PointerEventData eventData)
{
if (eventData.dragging || CurrentContainer == null) return;
switch (CurrentContainer.type)
{
case ContainerType.Deck:
GameManager.Instance.DrawCardByClickAsync().Forget();
break;
case ContainerType.Hand:
GameManager.Instance.PlayCardFromHandByClickAsync(this).Forget();
break;
case ContainerType.Discard:
GameManager.Instance.RecycleDiscardByClickAsync().Forget();
break;
}
}
private void CancelMotion(ref MotionHandle handle)
{
if (handle.IsActive()) handle.Cancel();
}
private void OnDestroy()
{
CancelMotion(ref _moveMotion);
CancelMotion(ref _scaleMotion);
CancelMotion(ref _rotMotion);
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 945efb5bdda04cfcbb6b00b1802d610f
timeCreated: 1793123373
+361
View File
@@ -0,0 +1,361 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using TMPro;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[Header("Containers")]
public CardContainer deckContainer;
public CardContainer handContainer;
public CardContainer boardContainer;
public CardContainer discardContainer;
[Header("Prefabs & UI")]
public CardView cardPrefab;
public TMP_Text statusText;
public TMP_Text scoreText;
[Header("Rules")]
public int maxHandSize = 4;
private int _playerScore;
private int _enemyScore;
private bool _isBusy;
private CancellationToken _destroyToken;
private void Awake()
{
Instance = this;
_destroyToken = this.GetCancellationTokenOnDestroy();
}
private void Start()
{
StartGameAsync().Forget();
}
private async UniTaskVoid StartGameAsync()
{
_isBusy = true;
UpdateScoreUI();
SetStatus("Подготовка колоды...");
CreateAndShuffleDeck();
await UniTask.Delay(400, cancellationToken: _destroyToken);
SetStatus("Раздаём карты...");
for (var i = 0; i < 3; i++)
{
await DrawTopCardToHandAsync();
}
SetStatus("Ваш ход: сыграйте карту на поле или сбросьте ненужную");
_isBusy = false;
}
private void CreateAndShuffleDeck()
{
var models = new List<CardModel>();
for (var copy = 0; copy < 2; copy++)
{
foreach (CardSuit suit in Enum.GetValues(typeof(CardSuit)))
{
foreach (CardRank rank in Enum.GetValues(typeof(CardRank)))
{
models.Add(new CardModel(suit, rank));
}
}
}
for (var i = models.Count - 1; i > 0; i--)
{
var rnd = UnityEngine.Random.Range(0, i + 1);
(models[i], models[rnd]) = (models[rnd], models[i]);
}
foreach (var model in models)
{
var card = Instantiate(cardPrefab, deckContainer.cardHolder);
card.Initialize(model, faceUp: false);
deckContainer.AddCardInstant(card);
}
}
public bool CanStartDrag(CardView card)
{
if (_isBusy || card.CurrentContainer == null) return false;
return card.CurrentContainer.type switch
{
ContainerType.Deck => deckContainer.Cards.Count > 0 && deckContainer.Cards.Contains(card),
ContainerType.Hand => true,
ContainerType.Board => true,
ContainerType.Discard => discardContainer.Cards.Count > 0 && discardContainer.Cards.Contains(card),
_ => false
};
}
public async UniTask HandleCardDropAsync(CardView card, CardContainer origin, CardContainer target, int returnIndex)
{
_isBusy = true;
if (target == null || target == origin)
{
await origin.AddCardAnimatedAsync(card, returnIndex, _destroyToken);
_isBusy = false;
return;
}
switch (origin.type)
{
case ContainerType.Deck when target.type == ContainerType.Hand:
{
if (handContainer.Cards.Count < maxHandSize)
{
await handContainer.AddCardAnimatedAsync(card, -1, _destroyToken);
SetStatus($"Взята карта: {FormatCardName(card.Model)}");
}
else
{
SetStatus($"В руке нет места (максимум {maxHandSize} карты)");
await origin.AddCardAnimatedAsync(card, returnIndex, _destroyToken);
}
break;
}
case ContainerType.Hand when target.type == ContainerType.Board:
await boardContainer.AddCardAnimatedAsync(card, -1, _destroyToken);
await ResolveBattleOnBoardAsync(card);
break;
case ContainerType.Hand when target.type == ContainerType.Discard:
await discardContainer.AddCardAnimatedAsync(card, -1, _destroyToken);
SetStatus($"Сброшен {FormatCardName(card.Model)}. Берём замену...");
await UniTask.Delay(250, cancellationToken: _destroyToken);
await DrawTopCardToHandAsync();
SetStatus("Ваш ход: выберите карту для атаки");
break;
case ContainerType.Discard when target.type == ContainerType.Deck:
await deckContainer.AddCardAnimatedAsync(card, -1, _destroyToken);
await RecycleDiscardToDeckAsync();
SetStatus("Сброшенные карты вернулись в колоду!");
break;
case ContainerType.Board when target.type == ContainerType.Discard:
await discardContainer.AddCardAnimatedAsync(card, -1, _destroyToken);
SetStatus("Карта отправлена в сброс");
break;
default:
SetStatus("Сюда карту положить нельзя");
await origin.AddCardAnimatedAsync(card, returnIndex, _destroyToken);
break;
}
_isBusy = false;
}
private async UniTask ResolveBattleOnBoardAsync(CardView playerCard)
{
SetStatus($"Вы сыграли: {FormatCardName(playerCard.Model)}. Ход противника...");
await UniTask.Delay(400, cancellationToken: _destroyToken);
if (deckContainer.Cards.Count == 0)
{
await RecycleDiscardToDeckAsync();
}
if (deckContainer.Cards.Count == 0)
{
SetStatus("В колоде закончились карты. Ничья в раунде!");
boardContainer.RemoveCardListReference(playerCard);
await discardContainer.AddCardAnimatedAsync(playerCard, -1, _destroyToken);
return;
}
var enemyCard = deckContainer.Cards[^1];
deckContainer.RemoveCardListReference(enemyCard);
await boardContainer.AddCardAnimatedAsync(enemyCard, -1, _destroyToken);
await UniTask.Delay(650, cancellationToken: _destroyToken);
var result = CompareCards(playerCard.Model, enemyCard.Model);
switch (result)
{
case > 0:
_playerScore++;
SoundManager.Instance?.PlayWin();
SetStatus($"Победа! Ваш {FormatCardName(playerCard.Model)} побеждает {FormatCardNameGenitive(enemyCard.Model)}!");
break;
case < 0:
_enemyScore++;
SoundManager.Instance?.PlayLose();
SetStatus($"Поражение: {FormatCardName(enemyCard.Model)} соперника оказался сильнее.");
break;
default:
SetStatus("Боевая ничья! Силы стихий равны.");
break;
}
UpdateScoreUI();
await UniTask.Delay(1100, cancellationToken: _destroyToken);
boardContainer.RemoveCardListReference(playerCard);
boardContainer.RemoveCardListReference(enemyCard);
await UniTask.WhenAll(
discardContainer.AddCardAnimatedAsync(playerCard, -1, _destroyToken),
discardContainer.AddCardAnimatedAsync(enemyCard, -1, _destroyToken)
);
if (handContainer.Cards.Count == 0)
{
if (deckContainer.Cards.Count == 0)
await RecycleDiscardToDeckAsync();
await DrawTopCardToHandAsync();
}
SetStatus("Ваш ход: выберите карту для атаки");
}
private int CompareCards(CardModel p, CardModel e)
{
if (p.rank != e.rank)
return p.rank > e.rank ? 1 : -1;
if (p.suit == e.suit)
return 0;
var playerSuitAdvantage =
(p.suit == CardSuit.Fire && e.suit == CardSuit.Earth) ||
(p.suit == CardSuit.Earth && e.suit == CardSuit.Air) ||
(p.suit == CardSuit.Air && e.suit == CardSuit.Water) ||
(p.suit == CardSuit.Water && e.suit == CardSuit.Fire);
return playerSuitAdvantage ? 1 : -1;
}
private async UniTask DrawTopCardToHandAsync()
{
if (deckContainer.Cards.Count == 0 || handContainer.Cards.Count >= maxHandSize) return;
var topCard = deckContainer.Cards[^1];
deckContainer.RemoveCardListReference(topCard);
await handContainer.AddCardAnimatedAsync(topCard, -1, _destroyToken);
}
private async UniTask RecycleDiscardToDeckAsync()
{
if (discardContainer.Cards.Count == 0) return;
SetStatus("Перетасовываем сброс обратно в колоду...");
var cardsToRecycle = new List<CardView>(discardContainer.Cards);
discardContainer.Cards.Clear();
foreach (var c in cardsToRecycle)
{
await deckContainer.AddCardAnimatedAsync(c, -1, _destroyToken);
}
}
public async UniTaskVoid DrawCardByClickAsync()
{
if (_isBusy) return;
if (handContainer.Cards.Count >= maxHandSize)
{
SetStatus($"В руке уже максимум карт ({maxHandSize})");
return;
}
_isBusy = true;
await DrawTopCardToHandAsync();
_isBusy = false;
}
public async UniTaskVoid PlayCardFromHandByClickAsync(CardView card)
{
if (_isBusy || card == null || card.CurrentContainer != handContainer) return;
_isBusy = true;
handContainer.RemoveCardListReference(card);
await boardContainer.AddCardAnimatedAsync(card, -1, _destroyToken);
await ResolveBattleOnBoardAsync(card);
_isBusy = false;
}
public async UniTaskVoid RecycleDiscardByClickAsync()
{
if (_isBusy) return;
if (discardContainer.Cards.Count == 0)
{
SetStatus("Стопка сброса пуста");
return;
}
_isBusy = true;
await RecycleDiscardToDeckAsync();
SetStatus("Колода пополнена из сброса!");
_isBusy = false;
}
private void UpdateScoreUI()
{
if (scoreText != null)
scoreText.text = $"Игрок: {_playerScore} | Противник: {_enemyScore}";
}
private void SetStatus(string message)
{
if (statusText != null)
statusText.text = message;
}
private static string FormatCardName(CardModel card)
{
var rank = card.rank switch
{
CardRank.Servant => "Слуга",
CardRank.Knight => "Рыцарь",
CardRank.Lord => "Владыка",
_ => "Воин"
};
var suit = card.suit switch
{
CardSuit.Fire => "Огня",
CardSuit.Earth => "Земли",
CardSuit.Air => "Воздуха",
CardSuit.Water => "Воды",
_ => ""
};
return $"{rank} {suit}";
}
private static string FormatCardNameGenitive(CardModel card)
{
var rank = card.rank switch
{
CardRank.Servant => "Слугу",
CardRank.Knight => "Рыцаря",
CardRank.Lord => "Владыку",
_ => "Воина"
};
var suit = card.suit switch
{
CardSuit.Fire => "Огня",
CardSuit.Earth => "Земли",
CardSuit.Air => "Воздуха",
CardSuit.Water => "Воды",
_ => ""
};
return $"{rank} {suit}";
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 58dd49544aba4ed3b32e9cdf36ab11c2
timeCreated: 1793123409
+35
View File
@@ -0,0 +1,35 @@
using UnityEngine;
[RequireComponent(typeof(AudioSource))]
public class SoundManager : MonoBehaviour
{
public static SoundManager Instance { get; private set; }
[Header("Audio Clips")]
public AudioClip cardDrawClip;
public AudioClip cardDropClip;
public AudioClip cardFlipClip;
public AudioClip winClip;
public AudioClip loseClip;
private AudioSource _audioSource;
private void Awake()
{
Instance = this;
_audioSource = GetComponent<AudioSource>();
}
public void PlayDraw() => PlayClip(cardDrawClip, 1.0f);
public void PlayDrop() => PlayClip(cardDropClip, 0.95f);
public void PlayFlip() => PlayClip(cardFlipClip, 1.1f);
public void PlayWin() => PlayClip(winClip, 1.0f);
public void PlayLose() => PlayClip(loseClip, 0.85f);
private void PlayClip(AudioClip clip, float pitch)
{
if (clip == null) return;
_audioSource.pitch = pitch + Random.Range(-0.05f, 0.05f);
_audioSource.PlayOneShot(clip);
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fe705acfef4341a8b11f68f9d8d29eb6
timeCreated: 1793123324