first commit

This commit is contained in:
2026-06-04 22:14:21 +03:00
commit 677f9b8090
139 changed files with 15981 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7b06c226736be0a9ca6bbd2946cb7fd2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5b84fdc271cff83cc985aac81f1ce2c1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,115 @@
using System.Collections;
using UnityEngine;
public class ExperiencePickup : MonoBehaviour
{
public int experienceAmount = 50;
public bool destroyOnCollect = true;
public float respawnCooldown = 12f;
public float rotationSpeed = 70f;
public LevelSystem levelSystem;
public NotificationUI notificationUI;
private bool playerInRange;
private bool collected;
private Renderer[] renderers;
private Collider[] colliders;
private void Awake()
{
renderers = GetComponentsInChildren<Renderer>();
colliders = GetComponentsInChildren<Collider>();
if (levelSystem == null)
{
levelSystem = FindObjectOfType<LevelSystem>();
}
if (notificationUI == null)
{
notificationUI = FindObjectOfType<NotificationUI>();
}
}
private void Update()
{
transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime, Space.World);
if (playerInRange && !collected && Input.GetKeyDown(KeyCode.E))
{
Collect();
}
}
private void OnTriggerEnter(Collider other)
{
if (other.GetComponentInParent<SimplePlayerController>() != null)
{
playerInRange = true;
if (notificationUI != null)
{
notificationUI.Show("Нажмите E, чтобы забрать кристалл опыта");
}
}
}
private void OnTriggerExit(Collider other)
{
if (other.GetComponentInParent<SimplePlayerController>() != null)
{
playerInRange = false;
}
}
private void Collect()
{
collected = true;
if (levelSystem != null)
{
levelSystem.AddExperience(experienceAmount);
}
if (notificationUI != null)
{
notificationUI.Show($"Кристалл опыта: +{experienceAmount} XP");
}
if (destroyOnCollect)
{
Destroy(gameObject);
}
else
{
StartCoroutine(RespawnRoutine());
}
}
private IEnumerator RespawnRoutine()
{
SetVisible(false);
yield return new WaitForSeconds(Mathf.Max(1f, respawnCooldown));
collected = false;
SetVisible(true);
}
private void SetVisible(bool visible)
{
for (int i = 0; i < renderers.Length; i++)
{
if (renderers[i] != null)
{
renderers[i].enabled = visible;
}
}
for (int i = 0; i < colliders.Length; i++)
{
if (colliders[i] != null)
{
colliders[i].enabled = visible;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 128b26ace3a81b67c825108a47f2a42c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+120
View File
@@ -0,0 +1,120 @@
using System;
using UnityEngine;
using UnityEngine.UI;
public class LevelSystem : MonoBehaviour
{
[Header("Progression")]
public int level = 1;
public int currentExp = 0;
public int expToNextLevel = 100;
[Header("Dependencies")]
public TalentManager talentManager;
public NotificationUI notificationUI;
[Header("HUD")]
public Text levelText;
public Text expText;
public Image expFillImage;
public event Action OnExperienceChanged;
public event Action OnLevelChanged;
private void Awake()
{
if (talentManager == null)
{
talentManager = FindObjectOfType<TalentManager>();
}
if (notificationUI == null)
{
notificationUI = FindObjectOfType<NotificationUI>();
}
}
private void Start()
{
RefreshUI();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.X))
{
AddExperience(50);
}
}
public void AddExperience(int amount)
{
if (amount <= 0)
{
return;
}
currentExp += amount;
Notify($"Получено опыта: {amount}");
while (currentExp >= expToNextLevel)
{
currentExp -= expToNextLevel;
LevelUp();
}
RefreshUI();
OnExperienceChanged?.Invoke();
}
public void LevelUp()
{
level = Mathf.Max(1, level + 1);
expToNextLevel = Mathf.Max(1, Mathf.RoundToInt(expToNextLevel * 1.25f + 25f));
if (talentManager != null)
{
talentManager.AddTalentPoints(1);
}
Notify($"Уровень {level}! Получено очко талантов");
RefreshUI();
OnLevelChanged?.Invoke();
}
public float GetExpNormalized()
{
if (expToNextLevel <= 0)
{
return 0f;
}
return Mathf.Clamp01((float)currentExp / expToNextLevel);
}
private void RefreshUI()
{
if (levelText != null)
{
levelText.text = $"Уровень {level}";
}
if (expText != null)
{
expText.text = $"{currentExp} / {expToNextLevel} XP";
}
if (expFillImage != null)
{
expFillImage.fillAmount = GetExpNormalized();
}
}
private void Notify(string message)
{
if (notificationUI != null)
{
notificationUI.Show(message);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ca4263d1e4dbc27e59409154d02bbb6d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,83 @@
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class SimplePlayerController : MonoBehaviour
{
public PlayerStats playerStats;
public CharacterController characterController;
public Transform cameraTransform;
[Header("Movement")]
public float gravity = -25f;
public float rotationSpeed = 12f;
[Header("Camera")]
public Vector3 cameraOffset = new Vector3(0f, 9f, -8f);
public float cameraYaw = 45f;
public float cameraSmooth = 12f;
private float verticalVelocity;
private void Awake()
{
if (characterController == null)
{
characterController = GetComponent<CharacterController>();
}
if (playerStats == null)
{
playerStats = GetComponent<PlayerStats>();
}
if (cameraTransform == null && Camera.main != null)
{
cameraTransform = Camera.main.transform;
}
}
private void Update()
{
if (Input.GetMouseButton(1))
{
cameraYaw += Input.GetAxis("Mouse X") * 120f * Time.deltaTime;
}
Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
input = Vector3.ClampMagnitude(input, 1f);
Quaternion yawRotation = Quaternion.Euler(0f, cameraYaw, 0f);
Vector3 moveDirection = yawRotation * input;
if (characterController.isGrounded && verticalVelocity < 0f)
{
verticalVelocity = -1f;
}
verticalVelocity += gravity * Time.deltaTime;
float speed = playerStats != null ? playerStats.MoveSpeed : 5f;
Vector3 velocity = moveDirection * speed;
velocity.y = verticalVelocity;
characterController.Move(velocity * Time.deltaTime);
if (moveDirection.sqrMagnitude > 0.001f)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDirection, Vector3.up);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
}
}
private void LateUpdate()
{
if (cameraTransform == null)
{
return;
}
Quaternion yawRotation = Quaternion.Euler(0f, cameraYaw, 0f);
Vector3 desiredPosition = transform.position + yawRotation * cameraOffset;
cameraTransform.position = Vector3.Lerp(cameraTransform.position, desiredPosition, cameraSmooth * Time.deltaTime);
cameraTransform.LookAt(transform.position + Vector3.up * 1.3f);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 23ff40a71fbf048f08376f5796c86de7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ee116732209f61cd3bffeba190d81c0d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+9
View File
@@ -0,0 +1,9 @@
using System;
using System.Collections.Generic;
[Serializable]
public class TalentSaveData
{
public int availableTalentPoints;
public List<string> learnedTalentIds = new List<string>();
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e9f0ba340003e18279312b5cde767c91
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4bc4b0205c2ec4bd09c773e84ba9deca
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+6
View File
@@ -0,0 +1,6 @@
public enum ModifierType
{
FlatAdd,
PercentAdd,
PercentMultiply
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 17d0bd4f5e38d384e85abdd7bb8a32db
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+213
View File
@@ -0,0 +1,213 @@
using System;
using System.Collections.Generic;
using UnityEngine;
public class PlayerStats : MonoBehaviour
{
[Header("Base Values")]
[SerializeField] private float baseMaxHealth = 100f;
[SerializeField] private float baseMaxMana = 100f;
[SerializeField] private float baseDamage = 10f;
[SerializeField] private float baseMoveSpeed = 5f;
[SerializeField] private float baseAbilityCooldownReduction = 0f;
[SerializeField] private float baseCriticalChance = 0f;
[Header("Runtime Modifiers")]
[SerializeField] private List<StatModifier> activeModifiers = new List<StatModifier>();
private readonly Dictionary<StatType, float> baseValues = new Dictionary<StatType, float>();
private readonly Dictionary<StatType, float> currentValues = new Dictionary<StatType, float>();
public event Action OnStatsChanged;
public float MaxHealth { get { return GetStat(StatType.MaxHealth); } }
public float MaxMana { get { return GetStat(StatType.MaxMana); } }
public float Damage { get { return GetStat(StatType.Damage); } }
public float MoveSpeed { get { return GetStat(StatType.MoveSpeed); } }
public float AbilityCooldownReduction { get { return GetStat(StatType.AbilityCooldownReduction); } }
public float CriticalChance { get { return GetStat(StatType.CriticalChance); } }
// Compatibility-friendly alias for lab wording: player speed is driven by PlayerStats.speed.
public float speed { get { return MoveSpeed; } }
private void Awake()
{
RecalculateStats();
}
private void OnValidate()
{
ClampBaseValues();
RecalculateStats();
}
public IReadOnlyList<StatModifier> GetActiveModifiers()
{
return activeModifiers;
}
public float GetBaseStat(StatType statType)
{
EnsureBaseValues();
return baseValues.TryGetValue(statType, out float value) ? value : 0f;
}
public float GetStat(StatType statType)
{
if (currentValues.Count == 0)
{
RecalculateStats();
}
return currentValues.TryGetValue(statType, out float value) ? value : GetBaseStat(statType);
}
public void AddModifier(StatModifier modifier)
{
if (!IsValidNumber(modifier.value))
{
Debug.LogWarning($"Ignored invalid modifier value for {modifier.statType}.");
return;
}
if (string.IsNullOrWhiteSpace(modifier.sourceId))
{
modifier.sourceId = "Runtime";
}
activeModifiers.Add(modifier);
RecalculateStats();
}
public int RemoveModifiersBySource(string sourceId)
{
if (string.IsNullOrWhiteSpace(sourceId))
{
return 0;
}
int removed = activeModifiers.RemoveAll(modifier => modifier.sourceId == sourceId);
if (removed > 0)
{
RecalculateStats();
}
return removed;
}
public int RemoveModifiersBySourcePrefix(string sourcePrefix)
{
if (string.IsNullOrWhiteSpace(sourcePrefix))
{
return 0;
}
int removed = activeModifiers.RemoveAll(modifier =>
!string.IsNullOrEmpty(modifier.sourceId) &&
modifier.sourceId.StartsWith(sourcePrefix, StringComparison.Ordinal));
if (removed > 0)
{
RecalculateStats();
}
return removed;
}
public void ClearModifiers()
{
if (activeModifiers.Count == 0)
{
return;
}
activeModifiers.Clear();
RecalculateStats();
}
public void RecalculateStats()
{
ClampBaseValues();
EnsureBaseValues();
currentValues.Clear();
foreach (StatType statType in Enum.GetValues(typeof(StatType)))
{
float baseValue = GetBaseStat(statType);
float flatAdd = 0f;
float percentAdd = 0f;
float percentMultiply = 1f;
for (int i = 0; i < activeModifiers.Count; i++)
{
StatModifier modifier = activeModifiers[i];
if (modifier.statType != statType || !IsValidNumber(modifier.value))
{
continue;
}
switch (modifier.modifierType)
{
case ModifierType.FlatAdd:
flatAdd += modifier.value;
break;
case ModifierType.PercentAdd:
percentAdd += modifier.value;
break;
case ModifierType.PercentMultiply:
percentMultiply *= Mathf.Max(0f, 1f + modifier.value);
break;
}
}
float finalValue = (baseValue + flatAdd) * Mathf.Max(0f, 1f + percentAdd) * percentMultiply;
currentValues[statType] = ClampFinalValue(statType, finalValue);
}
OnStatsChanged?.Invoke();
}
private void EnsureBaseValues()
{
baseValues[StatType.MaxHealth] = Mathf.Max(0f, baseMaxHealth);
baseValues[StatType.MaxMana] = Mathf.Max(0f, baseMaxMana);
baseValues[StatType.Damage] = Mathf.Max(0f, baseDamage);
baseValues[StatType.MoveSpeed] = Mathf.Max(0.1f, baseMoveSpeed);
baseValues[StatType.AbilityCooldownReduction] = Mathf.Clamp(baseAbilityCooldownReduction, 0f, 100f);
baseValues[StatType.CriticalChance] = Mathf.Clamp(baseCriticalChance, 0f, 100f);
}
private void ClampBaseValues()
{
baseMaxHealth = Mathf.Max(0f, baseMaxHealth);
baseMaxMana = Mathf.Max(0f, baseMaxMana);
baseDamage = Mathf.Max(0f, baseDamage);
baseMoveSpeed = Mathf.Max(0.1f, baseMoveSpeed);
baseAbilityCooldownReduction = Mathf.Clamp(baseAbilityCooldownReduction, 0f, 100f);
baseCriticalChance = Mathf.Clamp(baseCriticalChance, 0f, 100f);
}
private float ClampFinalValue(StatType statType, float value)
{
if (!IsValidNumber(value))
{
return GetBaseStat(statType);
}
switch (statType)
{
case StatType.MoveSpeed:
return Mathf.Max(0.1f, value);
case StatType.AbilityCooldownReduction:
case StatType.CriticalChance:
return Mathf.Clamp(value, 0f, 100f);
default:
return Mathf.Max(0f, value);
}
}
private bool IsValidNumber(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bea8fb1929119353ea53e65980138a53
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+18
View File
@@ -0,0 +1,18 @@
using System;
[Serializable]
public struct StatModifier
{
public StatType statType;
public ModifierType modifierType;
public float value;
public string sourceId;
public StatModifier(StatType statType, ModifierType modifierType, float value, string sourceId = "")
{
this.statType = statType;
this.modifierType = modifierType;
this.value = value;
this.sourceId = sourceId;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 48d0d2852923565a596a441444ff0153
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+9
View File
@@ -0,0 +1,9 @@
public enum StatType
{
MaxHealth,
MaxMana,
Damage,
MoveSpeed,
AbilityCooldownReduction,
CriticalChance
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f45916503d6cc198badac0de1d5845f5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 998a69656c6d3d932a74831b91629ba4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+25
View File
@@ -0,0 +1,25 @@
using UnityEngine;
[CreateAssetMenu(fileName = "TalentData", menuName = "Talents/TalentData")]
public class TalentData : ScriptableObject
{
public string talentId;
public string displayName;
[TextArea(2, 5)] public string description;
public Sprite icon;
public int cost = 1;
public TalentData[] prerequisites;
public StatModifier[] modifiers;
public Vector2 treePosition;
public Color branchColor = Color.white;
public bool unlockedByDefault;
private void OnValidate()
{
cost = Mathf.Max(0, cost);
if (string.IsNullOrWhiteSpace(talentId))
{
talentId = name;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7bd9889a6ab0a4e0a9a5e3d7a82343ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+408
View File
@@ -0,0 +1,408 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class TalentManager : MonoBehaviour
{
private const string SaveKey = "Lab07_TalentProgress";
private const string TalentSourcePrefix = "Talent:";
[Header("Data")]
public List<TalentData> allTalents = new List<TalentData>();
public PlayerStats playerStats;
[Header("Runtime Progress")]
public HashSet<string> learnedTalentIds = new HashSet<string>();
public int availableTalentPoints = 1;
public int startingTalentPoints = 1;
[Header("Feedback")]
public NotificationUI notificationUI;
private readonly Dictionary<string, TalentData> talentLookup = new Dictionary<string, TalentData>();
public event Action OnTalentStateChanged;
public event Action<string> OnNotificationRequested;
private void Awake()
{
if (playerStats == null)
{
playerStats = FindObjectOfType<PlayerStats>();
}
if (notificationUI == null)
{
notificationUI = FindObjectOfType<NotificationUI>();
}
BuildTalentLookup();
}
private void Start()
{
LoadProgress(false);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.T))
{
AddTalentPoints(1);
}
if (Input.GetKeyDown(KeyCode.F5))
{
SaveProgress();
}
if (Input.GetKeyDown(KeyCode.F9))
{
LoadProgress();
}
if (Input.GetKeyDown(KeyCode.Backspace))
{
ResetProgress();
}
}
public bool IsLearned(TalentData talent)
{
return IsValidTalent(talent) && learnedTalentIds.Contains(talent.talentId);
}
public bool CanLearn(TalentData talent)
{
return IsValidTalent(talent) &&
!IsLearned(talent) &&
availableTalentPoints >= Mathf.Max(0, talent.cost) &&
ArePrerequisitesLearned(talent);
}
public bool ArePrerequisitesLearned(TalentData talent)
{
if (!IsValidTalent(talent))
{
return false;
}
if (talent.prerequisites == null || talent.prerequisites.Length == 0)
{
return true;
}
for (int i = 0; i < talent.prerequisites.Length; i++)
{
TalentData prerequisite = talent.prerequisites[i];
if (!IsValidTalent(prerequisite) || !IsLearned(prerequisite))
{
return false;
}
}
return true;
}
public void LearnTalent(TalentData talent)
{
if (!IsValidTalent(talent))
{
Notify("Некорректный талант");
return;
}
if (IsLearned(talent))
{
Notify($"Талант уже изучен: {talent.displayName}");
return;
}
if (!ArePrerequisitesLearned(talent))
{
Notify(GetBlockingReason(talent));
return;
}
int cost = Mathf.Max(0, talent.cost);
if (availableTalentPoints < cost)
{
Notify("Недостаточно очков талантов");
return;
}
availableTalentPoints -= cost;
learnedTalentIds.Add(talent.talentId);
ApplyTalentModifiers(talent);
SaveProgress(false);
Notify($"Изучен талант: {talent.displayName}");
RaiseTalentStateChanged();
}
public void AddTalentPoints(int amount)
{
if (amount <= 0)
{
return;
}
availableTalentPoints = Mathf.Max(0, availableTalentPoints + amount);
Notify(amount == 1 ? "Получено очко талантов" : $"Получено очков талантов: {amount}");
RaiseTalentStateChanged();
}
public void ApplyTalentModifiers(TalentData talent)
{
if (!IsValidTalent(talent) || playerStats == null || talent.modifiers == null)
{
return;
}
string sourceId = GetModifierSourceId(talent);
playerStats.RemoveModifiersBySource(sourceId);
for (int i = 0; i < talent.modifiers.Length; i++)
{
StatModifier modifier = talent.modifiers[i];
modifier.sourceId = sourceId;
playerStats.AddModifier(modifier);
}
}
public void RebuildAllModifiers()
{
if (playerStats == null)
{
return;
}
playerStats.RemoveModifiersBySourcePrefix(TalentSourcePrefix);
for (int i = 0; i < allTalents.Count; i++)
{
TalentData talent = allTalents[i];
if (IsLearned(talent))
{
ApplyTalentModifiers(talent);
}
}
playerStats.RecalculateStats();
RaiseTalentStateChanged();
}
public void SaveProgress()
{
SaveProgress(true);
}
public void LoadProgress()
{
LoadProgress(true);
}
public void ResetProgress()
{
PlayerPrefs.DeleteKey(SaveKey);
ResetRuntimeToDefaults();
RebuildAllModifiers();
Notify("Прогресс сброшен");
RaiseTalentStateChanged();
}
public int GetAvailableTalentPoints()
{
return availableTalentPoints;
}
public List<TalentData> GetLearnedTalents()
{
BuildTalentLookup();
return allTalents.Where(IsLearned).ToList();
}
public string GetBlockingReason(TalentData talent)
{
if (!IsValidTalent(talent))
{
return "Некорректные данные таланта";
}
if (IsLearned(talent))
{
return "Талант уже изучен";
}
if (talent.prerequisites != null)
{
for (int i = 0; i < talent.prerequisites.Length; i++)
{
TalentData prerequisite = talent.prerequisites[i];
if (!IsValidTalent(prerequisite))
{
return "Некорректное требование таланта";
}
if (!IsLearned(prerequisite))
{
return $"Требуется талант: {prerequisite.displayName}";
}
}
}
if (availableTalentPoints < Mathf.Max(0, talent.cost))
{
return "Недостаточно очков талантов";
}
return "Доступно";
}
public TalentData GetTalentById(string talentId)
{
BuildTalentLookup();
if (string.IsNullOrWhiteSpace(talentId))
{
return null;
}
talentLookup.TryGetValue(talentId, out TalentData talent);
return talent;
}
private void SaveProgress(bool notify)
{
TalentSaveData saveData = new TalentSaveData
{
availableTalentPoints = Mathf.Max(0, availableTalentPoints),
learnedTalentIds = learnedTalentIds
.Where(id => !string.IsNullOrWhiteSpace(id))
.OrderBy(id => id, StringComparer.Ordinal)
.ToList()
};
PlayerPrefs.SetString(SaveKey, JsonUtility.ToJson(saveData));
PlayerPrefs.Save();
if (notify)
{
Notify("Прогресс сохранён");
}
}
private void LoadProgress(bool notify)
{
BuildTalentLookup();
learnedTalentIds.Clear();
if (PlayerPrefs.HasKey(SaveKey))
{
try
{
TalentSaveData saveData = JsonUtility.FromJson<TalentSaveData>(PlayerPrefs.GetString(SaveKey));
availableTalentPoints = Mathf.Max(0, saveData != null ? saveData.availableTalentPoints : startingTalentPoints);
if (saveData != null && saveData.learnedTalentIds != null)
{
for (int i = 0; i < saveData.learnedTalentIds.Count; i++)
{
string talentId = saveData.learnedTalentIds[i];
if (talentLookup.ContainsKey(talentId))
{
learnedTalentIds.Add(talentId);
}
}
}
}
catch (Exception exception)
{
Debug.LogWarning($"Talent progress was corrupted and will be reset. {exception.Message}");
ResetRuntimeToDefaults();
}
}
else
{
ResetRuntimeToDefaults();
}
EnsureDefaultTalentsLearned();
RebuildAllModifiers();
if (notify)
{
Notify("Прогресс загружен");
}
RaiseTalentStateChanged();
}
private void ResetRuntimeToDefaults()
{
learnedTalentIds.Clear();
availableTalentPoints = Mathf.Max(0, startingTalentPoints);
EnsureDefaultTalentsLearned();
}
private void EnsureDefaultTalentsLearned()
{
for (int i = 0; i < allTalents.Count; i++)
{
TalentData talent = allTalents[i];
if (IsValidTalent(talent) && talent.unlockedByDefault)
{
learnedTalentIds.Add(talent.talentId);
}
}
}
private void BuildTalentLookup()
{
talentLookup.Clear();
for (int i = 0; i < allTalents.Count; i++)
{
TalentData talent = allTalents[i];
if (!IsValidTalent(talent))
{
continue;
}
if (talentLookup.ContainsKey(talent.talentId))
{
Debug.LogWarning($"Duplicate talent id ignored: {talent.talentId}");
continue;
}
talentLookup.Add(talent.talentId, talent);
}
}
private bool IsValidTalent(TalentData talent)
{
return talent != null && !string.IsNullOrWhiteSpace(talent.talentId);
}
private string GetModifierSourceId(TalentData talent)
{
return TalentSourcePrefix + talent.talentId;
}
private void RaiseTalentStateChanged()
{
OnTalentStateChanged?.Invoke();
}
private void Notify(string message)
{
if (string.IsNullOrWhiteSpace(message))
{
return;
}
OnNotificationRequested?.Invoke(message);
if (notificationUI != null)
{
notificationUI.Show(message);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: edd6cb3143345fd4a982292c062fd6d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e8eaf4d60fca44a8a9193c9d0368fcb2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+75
View File
@@ -0,0 +1,75 @@
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(RectTransform))]
public class LineConnectorUI : MonoBehaviour
{
public RectTransform start;
public RectTransform end;
public Image lineImage;
public float thickness = 4f;
public bool dynamicUpdate = true;
private RectTransform rectTransform;
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
if (lineImage == null)
{
lineImage = GetComponent<Image>();
}
}
private void LateUpdate()
{
if (dynamicUpdate)
{
UpdateLine();
}
}
public void SetEndpoints(RectTransform startPoint, RectTransform endPoint)
{
start = startPoint;
end = endPoint;
UpdateLine();
}
public void UpdateLine()
{
if (start == null || end == null)
{
return;
}
if (rectTransform == null)
{
rectTransform = GetComponent<RectTransform>();
}
RectTransform parent = rectTransform.parent as RectTransform;
if (parent == null)
{
return;
}
Vector2 startPosition = GetCenterInParentSpace(parent, start);
Vector2 endPosition = GetCenterInParentSpace(parent, end);
Vector2 delta = endPosition - startPosition;
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.pivot = new Vector2(0.5f, 0.5f);
rectTransform.anchoredPosition = startPosition + delta * 0.5f;
rectTransform.sizeDelta = new Vector2(delta.magnitude, Mathf.Max(1f, thickness));
rectTransform.localRotation = Quaternion.Euler(0f, 0f, Mathf.Atan2(delta.y, delta.x) * Mathf.Rad2Deg);
}
private Vector2 GetCenterInParentSpace(RectTransform parent, RectTransform target)
{
Vector3 worldCenter = target.TransformPoint(target.rect.center);
Vector3 localCenter = parent.InverseTransformPoint(worldCenter);
return new Vector2(localCenter.x, localCenter.y);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91664eb39be68649fa272e155e08b460
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+67
View File
@@ -0,0 +1,67 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class NotificationUI : MonoBehaviour
{
public Text messageText;
public CanvasGroup canvasGroup;
public float visibleDuration = 2.2f;
public float fadeDuration = 0.25f;
private Coroutine routine;
private void Awake()
{
if (canvasGroup == null)
{
canvasGroup = GetComponent<CanvasGroup>();
}
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
canvasGroup.blocksRaycasts = false;
}
}
public void Show(string message)
{
if (messageText == null || string.IsNullOrWhiteSpace(message))
{
return;
}
messageText.text = message;
if (routine != null)
{
StopCoroutine(routine);
}
routine = StartCoroutine(ShowRoutine());
}
private IEnumerator ShowRoutine()
{
if (canvasGroup == null)
{
yield break;
}
canvasGroup.alpha = 1f;
yield return new WaitForSeconds(Mathf.Max(0.1f, visibleDuration));
float elapsed = 0f;
float duration = Mathf.Max(0.05f, fadeDuration);
while (elapsed < duration)
{
elapsed += Time.deltaTime;
canvasGroup.alpha = Mathf.Lerp(1f, 0f, elapsed / duration);
yield return null;
}
canvasGroup.alpha = 0f;
routine = null;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b9af78e14524d49c8942a7b8ab1dc61d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+75
View File
@@ -0,0 +1,75 @@
using UnityEngine;
using UnityEngine.UI;
public class StatsPanelUI : MonoBehaviour
{
public PlayerStats playerStats;
public Text titleText;
public Text maxHealthText;
public Text maxManaText;
public Text damageText;
public Text moveSpeedText;
public Text cooldownReductionText;
public Text criticalChanceText;
private void Awake()
{
if (playerStats == null)
{
playerStats = FindObjectOfType<PlayerStats>();
}
}
private void OnEnable()
{
if (playerStats != null)
{
playerStats.OnStatsChanged += Refresh;
}
Refresh();
}
private void OnDisable()
{
if (playerStats != null)
{
playerStats.OnStatsChanged -= Refresh;
}
}
public void Refresh()
{
if (playerStats == null)
{
playerStats = FindObjectOfType<PlayerStats>();
}
if (playerStats == null)
{
return;
}
if (titleText != null)
{
titleText.text = "Характеристики";
}
SetText(maxHealthText, "Max Health", playerStats.MaxHealth, false);
SetText(maxManaText, "Max Mana", playerStats.MaxMana, false);
SetText(damageText, "Damage", playerStats.Damage, false);
SetText(moveSpeedText, "Move Speed", playerStats.MoveSpeed, false);
SetText(cooldownReductionText, "Cooldown Reduction", playerStats.AbilityCooldownReduction, true);
SetText(criticalChanceText, "Critical Chance", playerStats.CriticalChance, true);
}
private void SetText(Text text, string label, float value, bool percent)
{
if (text == null)
{
return;
}
text.text = percent ? $"{label}: {value:0.#}%" : $"{label}: {value:0.#}";
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e2abe10fb9912e35b99a6e90c1840beb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+167
View File
@@ -0,0 +1,167 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class TalentNodeUI : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
public TalentData talent;
public TalentManager manager;
public TalentTooltipUI tooltip;
public Image backgroundImage;
public Image iconImage;
public Text nameText;
public Text costText;
public Text stateText;
public Button learnButton;
public Outline borderOutline;
private void Awake()
{
if (learnButton == null)
{
learnButton = GetComponent<Button>();
}
if (backgroundImage == null)
{
backgroundImage = GetComponent<Image>();
}
if (borderOutline == null)
{
borderOutline = GetComponent<Outline>();
}
}
private void OnEnable()
{
if (learnButton != null)
{
learnButton.onClick.RemoveListener(OnClickLearn);
learnButton.onClick.AddListener(OnClickLearn);
}
}
private void OnDisable()
{
if (learnButton != null)
{
learnButton.onClick.RemoveListener(OnClickLearn);
}
}
public void Setup(TalentData talent, TalentManager manager)
{
this.talent = talent;
this.manager = manager;
Refresh();
}
public void Refresh()
{
if (talent == null)
{
return;
}
if (nameText != null)
{
nameText.text = talent.displayName;
}
if (costText != null)
{
costText.text = $"{talent.cost} очк.";
}
if (iconImage != null)
{
iconImage.sprite = talent.icon;
iconImage.color = talent.icon != null ? Color.white : talent.branchColor;
}
bool learned = manager != null && manager.IsLearned(talent);
bool prerequisitesLearned = manager != null && manager.ArePrerequisitesLearned(talent);
bool enoughPoints = manager != null && manager.GetAvailableTalentPoints() >= talent.cost;
bool available = manager != null && manager.CanLearn(talent);
Color nodeColor = UITheme.LockedNode;
string state = "Заблокировано";
if (learned)
{
nodeColor = UITheme.LearnedNode;
state = "Изучено";
}
else if (available)
{
nodeColor = talent.branchColor;
state = "Доступно";
}
else if (prerequisitesLearned && !enoughPoints)
{
nodeColor = UITheme.NotEnoughNode;
state = "Не хватает очков";
}
if (backgroundImage != null)
{
backgroundImage.color = new Color(nodeColor.r, nodeColor.g, nodeColor.b, learned || available ? 0.92f : 0.72f);
}
if (borderOutline != null)
{
borderOutline.effectColor = learned ? UITheme.Gold : (available ? Color.white : UITheme.LockedNode);
}
if (stateText != null)
{
stateText.text = state;
}
if (learnButton != null)
{
learnButton.interactable = available;
}
}
public void OnClickLearn()
{
if (manager != null && talent != null)
{
manager.LearnTalent(talent);
}
}
public void ShowTooltip()
{
if (tooltip == null)
{
tooltip = FindObjectOfType<TalentTooltipUI>(true);
}
if (tooltip != null)
{
tooltip.Show(talent, manager);
}
}
public void HideTooltip()
{
if (tooltip != null)
{
tooltip.Hide();
}
}
public void OnPointerEnter(PointerEventData eventData)
{
ShowTooltip();
}
public void OnPointerExit(PointerEventData eventData)
{
HideTooltip();
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e2a5a7bbb12a718f3b66184c7d62d48f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+227
View File
@@ -0,0 +1,227 @@
using System.Text;
using UnityEngine;
using UnityEngine.UI;
public class TalentTooltipUI : MonoBehaviour
{
public RectTransform panelRoot;
public CanvasGroup canvasGroup;
public Text titleText;
public Text descriptionText;
public Text costText;
public Text modifiersText;
public Text requirementsText;
public Text statusText;
public Text reasonText;
public Vector2 screenOffset = new Vector2(22f, -18f);
private bool visible;
private void Awake()
{
if (panelRoot == null)
{
panelRoot = transform as RectTransform;
}
if (canvasGroup == null)
{
canvasGroup = GetComponent<CanvasGroup>();
}
Hide();
}
private void Update()
{
if (!visible || panelRoot == null)
{
return;
}
panelRoot.position = Input.mousePosition + new Vector3(screenOffset.x, screenOffset.y, 0f);
}
public void Show(TalentData talent, TalentManager manager)
{
if (talent == null)
{
return;
}
if (titleText != null)
{
titleText.text = talent.displayName;
}
if (descriptionText != null)
{
descriptionText.text = talent.description;
}
if (costText != null)
{
costText.text = $"Стоимость: {talent.cost}";
}
if (modifiersText != null)
{
modifiersText.text = BuildModifiersText(talent);
}
if (requirementsText != null)
{
requirementsText.text = BuildRequirementsText(talent);
}
if (statusText != null)
{
statusText.text = $"Статус: {BuildStatusText(talent, manager)}";
}
if (reasonText != null)
{
reasonText.text = manager != null ? manager.GetBlockingReason(talent) : string.Empty;
}
visible = true;
if (canvasGroup != null)
{
canvasGroup.alpha = 1f;
canvasGroup.blocksRaycasts = false;
}
if (panelRoot != null)
{
panelRoot.gameObject.SetActive(true);
panelRoot.SetAsLastSibling();
}
}
public void Hide()
{
visible = false;
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
canvasGroup.blocksRaycasts = false;
}
if (panelRoot != null)
{
panelRoot.gameObject.SetActive(false);
}
}
private string BuildModifiersText(TalentData talent)
{
if (talent.modifiers == null || talent.modifiers.Length == 0)
{
return "Модификаторы: нет";
}
StringBuilder builder = new StringBuilder();
builder.AppendLine("Модификаторы:");
for (int i = 0; i < talent.modifiers.Length; i++)
{
builder.AppendLine(FormatModifier(talent.modifiers[i]));
}
return builder.ToString().TrimEnd();
}
private string BuildRequirementsText(TalentData talent)
{
if (talent.prerequisites == null || talent.prerequisites.Length == 0)
{
return "Требования: нет";
}
StringBuilder builder = new StringBuilder("Требования: ");
for (int i = 0; i < talent.prerequisites.Length; i++)
{
if (i > 0)
{
builder.Append(", ");
}
TalentData prerequisite = talent.prerequisites[i];
builder.Append(prerequisite != null ? prerequisite.displayName : "Missing");
}
return builder.ToString();
}
private string BuildStatusText(TalentData talent, TalentManager manager)
{
if (manager == null)
{
return "неизвестно";
}
if (manager.IsLearned(talent))
{
return "изучено";
}
if (manager.CanLearn(talent))
{
return "доступно";
}
if (!manager.ArePrerequisitesLearned(talent))
{
return "заблокировано";
}
return "не хватает очков";
}
private string FormatModifier(StatModifier modifier)
{
string statName = FormatStatName(modifier.statType);
string valueText;
if (modifier.modifierType == ModifierType.PercentAdd || modifier.modifierType == ModifierType.PercentMultiply)
{
valueText = FormatSignedNumber(modifier.value * 100f) + "%";
}
else if (modifier.statType == StatType.AbilityCooldownReduction || modifier.statType == StatType.CriticalChance)
{
valueText = FormatSignedNumber(modifier.value) + "%";
}
else
{
valueText = FormatSignedNumber(modifier.value);
}
return $"{statName} {valueText}";
}
private string FormatStatName(StatType statType)
{
switch (statType)
{
case StatType.MaxHealth:
return "Max Health";
case StatType.MaxMana:
return "Max Mana";
case StatType.Damage:
return "Damage";
case StatType.MoveSpeed:
return "Move Speed";
case StatType.AbilityCooldownReduction:
return "Cooldown Reduction";
case StatType.CriticalChance:
return "Critical Chance";
default:
return statType.ToString();
}
}
private string FormatSignedNumber(float value)
{
string sign = value >= 0f ? "+" : string.Empty;
return sign + value.ToString("0.##");
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ad8bf819a87ddc4de9584bb11236e5a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+279
View File
@@ -0,0 +1,279 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TalentTreeUI : MonoBehaviour
{
public TalentManager manager;
public PlayerStats playerStats;
[Header("Tree")]
public GameObject panelRoot;
public RectTransform lineRoot;
public RectTransform nodesRoot;
public TalentNodeUI nodePrefab;
public TalentTooltipUI tooltip;
[Header("Panels")]
public Text titleText;
public Text pointsText;
public Text controlsText;
public StatsPanelUI statsPanel;
[Header("Buttons")]
public Button saveButton;
public Button loadButton;
public Button resetButton;
public bool openOnStart;
private readonly List<TalentNodeUI> nodes = new List<TalentNodeUI>();
private readonly List<LineConnectorUI> lines = new List<LineConnectorUI>();
private bool isOpen;
private void Awake()
{
if (manager == null)
{
manager = FindObjectOfType<TalentManager>();
}
if (playerStats == null)
{
playerStats = FindObjectOfType<PlayerStats>();
}
}
private void OnEnable()
{
if (manager != null)
{
manager.OnTalentStateChanged += Refresh;
}
}
private void OnDisable()
{
if (manager != null)
{
manager.OnTalentStateChanged -= Refresh;
}
}
private void Start()
{
BindButtons();
BuildTree();
Refresh();
SetOpen(openOnStart);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.K))
{
Toggle();
}
}
public void Toggle()
{
SetOpen(!isOpen);
}
public void SetOpen(bool open)
{
isOpen = open;
if (panelRoot != null)
{
panelRoot.SetActive(open);
}
if (open)
{
Refresh();
UpdateLines();
}
}
public void BuildTree()
{
ClearChildren(nodesRoot);
ClearChildren(lineRoot);
nodes.Clear();
lines.Clear();
if (manager == null || nodePrefab == null || nodesRoot == null)
{
return;
}
Dictionary<TalentData, TalentNodeUI> nodeByTalent = new Dictionary<TalentData, TalentNodeUI>();
for (int i = 0; i < manager.allTalents.Count; i++)
{
TalentData talent = manager.allTalents[i];
if (talent == null)
{
continue;
}
TalentNodeUI node = Instantiate(nodePrefab, nodesRoot);
node.gameObject.SetActive(true);
node.tooltip = tooltip;
node.Setup(talent, manager);
RectTransform rectTransform = node.transform as RectTransform;
if (rectTransform != null)
{
rectTransform.anchoredPosition = talent.treePosition;
}
nodes.Add(node);
nodeByTalent[talent] = node;
}
if (lineRoot == null)
{
return;
}
for (int i = 0; i < manager.allTalents.Count; i++)
{
TalentData talent = manager.allTalents[i];
if (talent == null || talent.prerequisites == null)
{
continue;
}
for (int prerequisiteIndex = 0; prerequisiteIndex < talent.prerequisites.Length; prerequisiteIndex++)
{
TalentData prerequisite = talent.prerequisites[prerequisiteIndex];
if (prerequisite == null || !nodeByTalent.ContainsKey(prerequisite) || !nodeByTalent.ContainsKey(talent))
{
continue;
}
LineConnectorUI line = CreateLine(lineRoot, prerequisite, nodeByTalent[prerequisite], nodeByTalent[talent]);
lines.Add(line);
}
}
UpdateLines();
}
public void Refresh()
{
if (titleText != null)
{
titleText.text = "Дерево талантов";
}
if (pointsText != null)
{
int points = manager != null ? manager.GetAvailableTalentPoints() : 0;
pointsText.text = $"Очки талантов: {points}";
}
if (controlsText != null)
{
controlsText.text = "K - дерево X - +50 XP T - +1 очко E - кристалл F5 - сохранить F9 - загрузить Backspace - сброс";
}
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i] != null)
{
nodes[i].Refresh();
}
}
if (statsPanel != null)
{
statsPanel.Refresh();
}
UpdateLines();
}
private void BindButtons()
{
if (saveButton != null)
{
saveButton.onClick.RemoveAllListeners();
saveButton.onClick.AddListener(() =>
{
if (manager != null)
{
manager.SaveProgress();
}
});
}
if (loadButton != null)
{
loadButton.onClick.RemoveAllListeners();
loadButton.onClick.AddListener(() =>
{
if (manager != null)
{
manager.LoadProgress();
}
});
}
if (resetButton != null)
{
resetButton.onClick.RemoveAllListeners();
resetButton.onClick.AddListener(() =>
{
if (manager != null)
{
manager.ResetProgress();
}
});
}
}
private LineConnectorUI CreateLine(RectTransform parent, TalentData childTalent, TalentNodeUI startNode, TalentNodeUI endNode)
{
GameObject lineObject = new GameObject($"Line_{startNode.talent.talentId}_to_{childTalent.talentId}", typeof(RectTransform), typeof(Image), typeof(LineConnectorUI));
lineObject.transform.SetParent(parent, false);
Image image = lineObject.GetComponent<Image>();
Color color = childTalent.branchColor;
image.color = new Color(color.r, color.g, color.b, 0.72f);
image.raycastTarget = false;
LineConnectorUI connector = lineObject.GetComponent<LineConnectorUI>();
connector.lineImage = image;
connector.thickness = 5f;
connector.dynamicUpdate = true;
connector.SetEndpoints(startNode.transform as RectTransform, endNode.transform as RectTransform);
return connector;
}
private void UpdateLines()
{
Canvas.ForceUpdateCanvases();
for (int i = 0; i < lines.Count; i++)
{
if (lines[i] != null)
{
lines[i].UpdateLine();
}
}
}
private void ClearChildren(RectTransform root)
{
if (root == null)
{
return;
}
for (int i = root.childCount - 1; i >= 0; i--)
{
Destroy(root.GetChild(i).gameObject);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d35bf66a09a03cd7ab53136fcb7d2a1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+15
View File
@@ -0,0 +1,15 @@
using UnityEngine;
public static class UITheme
{
public static readonly Color Panel = new Color(0.06f, 0.07f, 0.09f, 0.92f);
public static readonly Color PanelSoft = new Color(0.09f, 0.10f, 0.13f, 0.86f);
public static readonly Color Gold = new Color(1.00f, 0.72f, 0.24f, 1f);
public static readonly Color Text = new Color(0.94f, 0.92f, 0.86f, 1f);
public static readonly Color MutedText = new Color(0.70f, 0.72f, 0.76f, 1f);
public static readonly Color LearnedNode = new Color(0.19f, 0.66f, 0.34f, 1f);
public static readonly Color AvailableNode = new Color(0.86f, 0.58f, 0.18f, 1f);
public static readonly Color LockedNode = new Color(0.28f, 0.29f, 0.32f, 1f);
public static readonly Color NotEnoughNode = new Color(0.14f, 0.15f, 0.18f, 1f);
public static readonly Color Danger = new Color(0.90f, 0.28f, 0.23f, 1f);
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1ce84f6aceb15dff29ba0e6028b52677
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: