commit 677f9b8090a1d8f5c2fb901348a0bc2542553949 Author: Dmitry Evdokimov Date: Thu Jun 4 22:14:21 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dae2c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Unity generated folders +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Bb]uild/ +[Bb]uilds/ +[Ll]ogs/ +[Uu]ser[Ss]ettings/ +[Mm]emoryCaptures/ +[Rr]ecordings/ + +# Unity generated files +*.csproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db + +# Unity crash and profiling artifacts +sysinfo.txt +*.stackdump + +# IDE and editor folders +.vs/ +.vscode/ +.idea/ +*.swp +*.swo +*.swn + +# OS metadata +.DS_Store +Thumbs.db +ehthumbs.db +Desktop.ini + +# Logs and local runtime output +*.log +*.trace + +# Generated reports and office documents +*.docx +*.doc +*.pdf + +# Archives and build packages +*.zip +*.7z +*.rar +*.tar +*.tar.gz + +# Unity asset store tools +Assets/AssetStoreTools* + +# Do not ignore Unity metadata. Meta files must be committed with assets. +!*.meta diff --git a/Assets/Editor.meta b/Assets/Editor.meta new file mode 100644 index 0000000..d72554d --- /dev/null +++ b/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 92152e5720e2fb22cbda001251c5bd44 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Lab7SceneBuilder.cs b/Assets/Editor/Lab7SceneBuilder.cs new file mode 100644 index 0000000..bd8b877 --- /dev/null +++ b/Assets/Editor/Lab7SceneBuilder.cs @@ -0,0 +1,1153 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.SceneManagement; +using UnityEngine.UI; + +public static class Lab7SceneBuilder +{ + private const string ScenePath = "Assets/_Scenes/Lab07_TalentSystem.unity"; + private const string NodePrefabPath = "Assets/_Prefabs/UI/TalentNodeUI.prefab"; + private const string TalentIconFolder = "Assets/_Data/Talents/Icons"; + + [MenuItem("Tools/Technical Game Design/Lab7/Create Talent System Scene")] + public static void CreateTalentSystemScene() + { + EnsureProjectFolders(); + + Font regularFont = InstallUbuntuFont( + "Ubuntu-Regular.ttf", + new[] + { + "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf", + "/usr/share/fonts/TTF/Ubuntu-R.ttf", + "/usr/share/fonts/Ubuntu-R.ttf" + }, + "regular"); + + Font boldFont = InstallUbuntuFont( + "Ubuntu-Bold.ttf", + new[] + { + "/usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf", + "/usr/share/fonts/TTF/Ubuntu-B.ttf", + "/usr/share/fonts/Ubuntu-B.ttf" + }, + "bold"); + + if (boldFont == null) + { + boldFont = regularFont; + } + + Material arenaMaterial = CreateMaterial("Assets/_Materials/Arena_Ground.mat", new Color(0.22f, 0.27f, 0.23f, 1f)); + Material playerMaterial = CreateMaterial("Assets/_Materials/Player_Gold.mat", new Color(0.92f, 0.62f, 0.18f, 1f)); + Material mannequinMaterial = CreateMaterial("Assets/_Materials/Training_Dummy.mat", new Color(0.48f, 0.34f, 0.22f, 1f)); + Material crystalMaterial = CreateMaterial("Assets/_Materials/Experience_Crystal.mat", new Color(0.16f, 0.78f, 0.96f, 1f), true); + Material decorMaterial = CreateMaterial("Assets/_Materials/Decor_Stone.mat", new Color(0.34f, 0.36f, 0.38f, 1f)); + + Dictionary talents = CreateTalentAssets(); + TalentNodeUI nodePrefab = CreateTalentNodePrefab(regularFont, boldFont); + + Scene scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + scene.name = "Lab07_TalentSystem"; + + ConfigureSceneLighting(); + Camera mainCamera = CreateCamera(); + + PlayerStats playerStats = CreatePlayer(playerMaterial, mainCamera); + GameObject systemsObject = new GameObject("GameSystems"); + TalentManager talentManager = systemsObject.AddComponent(); + talentManager.playerStats = playerStats; + talentManager.startingTalentPoints = 1; + talentManager.availableTalentPoints = 1; + talentManager.allTalents = new List(talents.Values); + + LevelSystem levelSystem = systemsObject.AddComponent(); + levelSystem.talentManager = talentManager; + + CreateArena(arenaMaterial); + CreateTrainingDummies(mannequinMaterial); + CreateDecorations(decorMaterial); + + Canvas canvas = CreateCanvas(); + EnsureEventSystem(); + + NotificationUI notificationUI = CreateNotificationUI(canvas.transform, regularFont); + talentManager.notificationUI = notificationUI; + levelSystem.notificationUI = notificationUI; + + CreateLevelHud(canvas.transform, regularFont, boldFont, levelSystem); + CreateStatsHud(canvas.transform, regularFont, boldFont, playerStats); + CreateTalentTree(canvas.transform, regularFont, boldFont, nodePrefab, talentManager, playerStats); + CreateExperiencePickups(crystalMaterial, levelSystem, notificationUI); + + EditorUtility.SetDirty(talentManager); + EditorUtility.SetDirty(levelSystem); + + EditorSceneManager.SaveScene(scene, ScenePath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + Debug.Log($"Lab 7 talent system scene created: {ScenePath}"); + } + + [MenuItem("Tools/Technical Game Design/Lab7/Validate Generated Scene")] + public static void ValidateGeneratedScene() + { + if (!File.Exists(ToSystemPath(ScenePath))) + { + Debug.LogError($"Scene does not exist: {ScenePath}"); + return; + } + + Scene scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single); + int errors = 0; + + PlayerStats playerStats = UnityEngine.Object.FindObjectOfType(true); + TalentManager talentManager = UnityEngine.Object.FindObjectOfType(true); + LevelSystem levelSystem = UnityEngine.Object.FindObjectOfType(true); + TalentTreeUI talentTreeUI = UnityEngine.Object.FindObjectOfType(true); + NotificationUI notificationUI = UnityEngine.Object.FindObjectOfType(true); + + errors += RequireObject(playerStats, "PlayerStats"); + errors += RequireObject(talentManager, "TalentManager"); + errors += RequireObject(levelSystem, "LevelSystem"); + errors += RequireObject(talentTreeUI, "TalentTreeUI"); + errors += RequireObject(notificationUI, "NotificationUI"); + + if (talentManager != null) + { + errors += RequireObject(talentManager.playerStats, "TalentManager.playerStats"); + errors += RequireObject(talentManager.notificationUI, "TalentManager.notificationUI"); + errors += RequireCondition(talentManager.allTalents != null && talentManager.allTalents.Count == 10, "TalentManager must reference exactly 10 talents."); + errors += ValidateTalentPrerequisites(talentManager); + } + + if (levelSystem != null) + { + errors += RequireObject(levelSystem.talentManager, "LevelSystem.talentManager"); + errors += RequireObject(levelSystem.notificationUI, "LevelSystem.notificationUI"); + errors += RequireObject(levelSystem.levelText, "LevelSystem.levelText"); + errors += RequireObject(levelSystem.expText, "LevelSystem.expText"); + errors += RequireObject(levelSystem.expFillImage, "LevelSystem.expFillImage"); + } + + if (talentTreeUI != null) + { + errors += RequireObject(talentTreeUI.panelRoot, "TalentTreeUI.panelRoot"); + errors += RequireObject(talentTreeUI.lineRoot, "TalentTreeUI.lineRoot"); + errors += RequireObject(talentTreeUI.nodesRoot, "TalentTreeUI.nodesRoot"); + errors += RequireObject(talentTreeUI.nodePrefab, "TalentTreeUI.nodePrefab"); + errors += RequireObject(talentTreeUI.tooltip, "TalentTreeUI.tooltip"); + errors += RequireObject(talentTreeUI.statsPanel, "TalentTreeUI.statsPanel"); + errors += RequireObject(talentTreeUI.saveButton, "TalentTreeUI.saveButton"); + errors += RequireObject(talentTreeUI.loadButton, "TalentTreeUI.loadButton"); + errors += RequireObject(talentTreeUI.resetButton, "TalentTreeUI.resetButton"); + } + + errors += ValidateMissingScripts(scene); + errors += ValidateUbuntuFonts(scene); + + if (errors == 0) + { + Debug.Log("Lab 7 validation passed."); + } + else + { + Debug.LogError($"Lab 7 validation failed. Errors: {errors}"); + } + } + + private static void EnsureProjectFolders() + { + string[] folders = + { + "Assets/_Scenes", + "Assets/_Scripts", + "Assets/_Scripts/Core", + "Assets/_Scripts/Stats", + "Assets/_Scripts/Talents", + "Assets/_Scripts/UI", + "Assets/_Scripts/Gameplay", + "Assets/_Scripts/Save", + "Assets/_Data", + "Assets/_Data/Talents", + TalentIconFolder, + "Assets/_Prefabs", + "Assets/_Prefabs/UI", + "Assets/_Materials", + "Assets/_Fonts", + "Assets/Editor" + }; + + for (int i = 0; i < folders.Length; i++) + { + if (!Directory.Exists(folders[i])) + { + Directory.CreateDirectory(folders[i]); + } + } + + AssetDatabase.Refresh(); + } + + private static Font InstallUbuntuFont(string destinationFileName, string[] preferredPaths, string label) + { + string destinationPath = "Assets/_Fonts/" + destinationFileName; + string sourcePath = FindExistingFile(preferredPaths); + + if (string.IsNullOrEmpty(sourcePath)) + { + sourcePath = FindFontRecursive(destinationFileName.Contains("Bold") ? "Ubuntu-B.ttf" : "Ubuntu-R.ttf"); + } + + if (!string.IsNullOrEmpty(sourcePath)) + { + File.Copy(sourcePath, destinationPath, true); + AssetDatabase.ImportAsset(destinationPath, ImportAssetOptions.ForceUpdate); + Font font = AssetDatabase.LoadAssetAtPath(destinationPath); + if (font != null) + { + return font; + } + } + + Debug.LogWarning($"Ubuntu {label} font was not found in system font folders. Arial fallback will be used."); + return Resources.GetBuiltinResource("Arial.ttf"); + } + + private static string FindExistingFile(string[] paths) + { + for (int i = 0; i < paths.Length; i++) + { + if (File.Exists(paths[i])) + { + return paths[i]; + } + } + + return null; + } + + private static string FindFontRecursive(string fileName) + { + string[] roots = { "/usr/share/fonts", "/usr/share/fonts/truetype/ubuntu", "/usr/share/fonts/TTF" }; + for (int i = 0; i < roots.Length; i++) + { + if (!Directory.Exists(roots[i])) + { + continue; + } + + string[] matches = Directory.GetFiles(roots[i], fileName, SearchOption.AllDirectories); + if (matches.Length > 0) + { + return matches[0]; + } + } + + return null; + } + + private static Material CreateMaterial(string path, Color color, bool emission = false) + { + Material material = AssetDatabase.LoadAssetAtPath(path); + if (material == null) + { + material = new Material(Shader.Find("Standard")); + AssetDatabase.CreateAsset(material, path); + } + + material.color = color; + if (emission) + { + material.EnableKeyword("_EMISSION"); + material.SetColor("_EmissionColor", color * 1.8f); + } + else + { + material.DisableKeyword("_EMISSION"); + } + + EditorUtility.SetDirty(material); + return material; + } + + private static Dictionary CreateTalentAssets() + { + Color toughnessColor = new Color(0.27f, 0.78f, 0.42f, 1f); + Color strengthColor = new Color(0.92f, 0.46f, 0.24f, 1f); + Color agilityColor = new Color(0.24f, 0.76f, 0.86f, 1f); + Color manaColor = new Color(0.42f, 0.52f, 0.96f, 1f); + Color masteryColor = new Color(1.00f, 0.72f, 0.24f, 1f); + + TalentSpec[] specs = + { + new TalentSpec( + "Toughness", + "Выносливость", + "Увеличивает максимальное здоровье персонажа.", + 1, + new Vector2(-340f, 170f), + toughnessColor, + new StatModifier(StatType.MaxHealth, ModifierType.FlatAdd, 20f), + Array.Empty()), + new TalentSpec( + "Strength", + "Сила", + "Увеличивает базовый урон персонажа.", + 1, + new Vector2(-90f, 170f), + strengthColor, + new StatModifier(StatType.Damage, ModifierType.FlatAdd, 5f), + Array.Empty()), + new TalentSpec( + "Agility", + "Ловкость", + "Увеличивает скорость передвижения.", + 1, + new Vector2(160f, 170f), + agilityColor, + new StatModifier(StatType.MoveSpeed, ModifierType.FlatAdd, 1f), + Array.Empty()), + new TalentSpec( + "ManaFlow", + "Поток маны", + "Увеличивает максимальный запас маны.", + 1, + new Vector2(410f, 170f), + manaColor, + new StatModifier(StatType.MaxMana, ModifierType.FlatAdd, 25f), + Array.Empty()), + new TalentSpec( + "AdvancedToughness", + "Улучшенная выносливость", + "Дополнительно усиливает запас здоровья.", + 1, + new Vector2(-340f, 35f), + toughnessColor, + new StatModifier(StatType.MaxHealth, ModifierType.FlatAdd, 30f), + new[] { "Toughness" }), + new TalentSpec( + "BattleTraining", + "Боевая подготовка", + "Повышает весь наносимый урон на 10%.", + 1, + new Vector2(-90f, 35f), + strengthColor, + new StatModifier(StatType.Damage, ModifierType.PercentAdd, 0.10f), + new[] { "Strength" }), + new TalentSpec( + "CriticalEye", + "Меткий взгляд", + "Увеличивает шанс критического удара.", + 1, + new Vector2(35f, -100f), + strengthColor, + new StatModifier(StatType.CriticalChance, ModifierType.FlatAdd, 5f), + new[] { "Strength" }), + new TalentSpec( + "SwiftStep", + "Быстрый шаг", + "Повышает скорость передвижения на 10%.", + 1, + new Vector2(160f, 35f), + agilityColor, + new StatModifier(StatType.MoveSpeed, ModifierType.PercentAdd, 0.10f), + new[] { "Agility" }), + new TalentSpec( + "ArcaneFocus", + "Магическая концентрация", + "Сокращает время восстановления способностей на 10%.", + 1, + new Vector2(410f, 35f), + manaColor, + new StatModifier(StatType.AbilityCooldownReduction, ModifierType.FlatAdd, 10f), + new[] { "ManaFlow" }), + new TalentSpec( + "MasterWarrior", + "Мастер-воин", + "Финальный боевой талант: больше урона и здоровья.", + 2, + new Vector2(-220f, -185f), + masteryColor, + new[] + { + new StatModifier(StatType.Damage, ModifierType.PercentAdd, 0.15f), + new StatModifier(StatType.MaxHealth, ModifierType.FlatAdd, 10f) + }, + new[] { "BattleTraining", "AdvancedToughness" }) + }; + + Dictionary assets = new Dictionary(); + for (int i = 0; i < specs.Length; i++) + { + TalentSpec spec = specs[i]; + TalentData talent = LoadOrCreateTalent(spec.id); + talent.talentId = spec.id; + talent.displayName = spec.displayName; + talent.description = spec.description; + talent.cost = spec.cost; + talent.treePosition = spec.treePosition; + talent.branchColor = spec.branchColor; + talent.modifiers = spec.modifiers; + talent.unlockedByDefault = false; + talent.icon = CreateTalentIcon(spec.id, spec.branchColor); + assets[spec.id] = talent; + } + + for (int i = 0; i < specs.Length; i++) + { + TalentSpec spec = specs[i]; + TalentData talent = assets[spec.id]; + TalentData[] prerequisites = new TalentData[spec.prerequisiteIds.Length]; + for (int prerequisiteIndex = 0; prerequisiteIndex < spec.prerequisiteIds.Length; prerequisiteIndex++) + { + prerequisites[prerequisiteIndex] = assets[spec.prerequisiteIds[prerequisiteIndex]]; + } + + talent.prerequisites = prerequisites; + EditorUtility.SetDirty(talent); + } + + AssetDatabase.SaveAssets(); + return assets; + } + + private static TalentData LoadOrCreateTalent(string id) + { + string path = $"Assets/_Data/Talents/{id}.asset"; + TalentData talent = AssetDatabase.LoadAssetAtPath(path); + if (talent == null) + { + talent = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(talent, path); + } + + return talent; + } + + private static Sprite CreateTalentIcon(string id, Color color) + { + string iconPath = $"{TalentIconFolder}/{id}_Icon.png"; + Texture2D texture = new Texture2D(64, 64, TextureFormat.RGBA32, false); + Color clear = new Color(0f, 0f, 0f, 0f); + Vector2 center = new Vector2(31.5f, 31.5f); + + for (int y = 0; y < 64; y++) + { + for (int x = 0; x < 64; x++) + { + float distance = Vector2.Distance(new Vector2(x, y), center); + if (distance > 29f) + { + texture.SetPixel(x, y, clear); + } + else if (distance > 25f) + { + texture.SetPixel(x, y, UITheme.Gold); + } + else if (distance < 8f) + { + texture.SetPixel(x, y, Color.white); + } + else + { + texture.SetPixel(x, y, color); + } + } + } + + texture.Apply(); + File.WriteAllBytes(ToSystemPath(iconPath), texture.EncodeToPNG()); + UnityEngine.Object.DestroyImmediate(texture); + AssetDatabase.ImportAsset(iconPath, ImportAssetOptions.ForceUpdate); + + TextureImporter importer = AssetImporter.GetAtPath(iconPath) as TextureImporter; + if (importer != null) + { + importer.textureType = TextureImporterType.Sprite; + importer.spriteImportMode = SpriteImportMode.Single; + importer.alphaIsTransparency = true; + importer.mipmapEnabled = false; + importer.SaveAndReimport(); + } + + return AssetDatabase.LoadAssetAtPath(iconPath); + } + + private static TalentNodeUI CreateTalentNodePrefab(Font regularFont, Font boldFont) + { + GameObject root = new GameObject("TalentNodeUI", typeof(RectTransform), typeof(Image), typeof(Button), typeof(Outline), typeof(TalentNodeUI)); + RectTransform rootRect = root.GetComponent(); + rootRect.sizeDelta = new Vector2(166f, 94f); + + Image background = root.GetComponent(); + background.color = UITheme.LockedNode; + background.raycastTarget = true; + + Button button = root.GetComponent