commit 6318b20158ced1c71eec0137ce78fa1b91d5020a Author: Dmitry Evdokimov Date: Thu Jun 4 19:34:38 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61e8fcb --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +# Unity generated folders +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Bb]uild/ +[Bb]uilds/ +[Ll]ogs/ +[Uu]ser[Ss]ettings/ +[Mm]emoryCaptures/ + +# Unity asset/import artifacts that should not be versioned +*.csproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db +*.pidb.meta +sysinfo.txt + +# Unity crash and profiler outputs +*.stackdump +*.dmp +*.raw +*.mem + +# Build outputs and packages +*.apk +*.aab +*.unitypackage +*.app + +# Addressables / streaming generated content +[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* +[Aa]ssets/[Ss]treamingAssets/aa.meta +[Aa]ssets/[Ss]treamingAssets/aa/ + +# Visual Studio / Rider / VS Code local state +.vs/ +.idea/ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.DotSettings.user + +# OS files +.DS_Store +Thumbs.db +Desktop.ini + +# Python/local tooling caches +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ + +# Lab reports and office temporary files +*.docx +~$*.docx +*.tmp.docx diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ddb6ff8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "visualstudiotoolsforunity.vstuc" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..da60e25 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,10 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Unity", + "type": "vstuc", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2a17750 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,55 @@ +{ + "files.exclude": { + "**/.DS_Store": true, + "**/.git": true, + "**/.gitmodules": true, + "**/*.booproj": true, + "**/*.pidb": true, + "**/*.suo": true, + "**/*.user": true, + "**/*.userprefs": true, + "**/*.unityproj": true, + "**/*.dll": true, + "**/*.exe": true, + "**/*.pdf": true, + "**/*.mid": true, + "**/*.midi": true, + "**/*.wav": true, + "**/*.gif": true, + "**/*.ico": true, + "**/*.jpg": true, + "**/*.jpeg": true, + "**/*.png": true, + "**/*.psd": true, + "**/*.tga": true, + "**/*.tif": true, + "**/*.tiff": true, + "**/*.3ds": true, + "**/*.3DS": true, + "**/*.fbx": true, + "**/*.FBX": true, + "**/*.lxo": true, + "**/*.LXO": true, + "**/*.ma": true, + "**/*.MA": true, + "**/*.obj": true, + "**/*.OBJ": true, + "**/*.asset": true, + "**/*.cubemap": true, + "**/*.flare": true, + "**/*.mat": true, + "**/*.meta": true, + "**/*.prefab": true, + "**/*.unity": true, + "build/": true, + "Build/": true, + "Library/": true, + "library/": true, + "obj/": true, + "Obj/": true, + "ProjectSettings/": true, + "temp/": true, + "Temp/": true + }, + "dotnet.defaultSolution": "OTG_Lab04_DialogueSystem.sln" +} \ No newline at end of file diff --git a/.xdp-.~4_Система_диалогов_ОТГ.docx-4vBNF7 b/.xdp-.~4_Система_диалогов_ОТГ.docx-4vBNF7 new file mode 100644 index 0000000..b0f443f Binary files /dev/null and b/.xdp-.~4_Система_диалогов_ОТГ.docx-4vBNF7 differ diff --git a/Assets/Editor.meta b/Assets/Editor.meta new file mode 100644 index 0000000..43dc2ed --- /dev/null +++ b/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f204c61df8aefbf72a0d2d40f00aa2c8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Lab4SceneBuilder.cs b/Assets/Editor/Lab4SceneBuilder.cs new file mode 100644 index 0000000..97d5800 --- /dev/null +++ b/Assets/Editor/Lab4SceneBuilder.cs @@ -0,0 +1,776 @@ +using System.IO; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.SceneManagement; +using UnityEngine.UI; + +public static class Lab4SceneBuilder +{ + private const string ScriptsPath = "Assets/_Scripts"; + private const string DataPath = "Assets/_Data"; + private const string DialoguePath = "Assets/_Data/Dialogue"; + private const string ItemsPath = "Assets/_Data/Items"; + private const string PrefabsPath = "Assets/_Prefabs"; + private const string ScenesPath = "Assets/_Scenes"; + private const string MaterialsPath = "Assets/_Materials"; + private const string FontsPath = "Assets/_Fonts"; + private const string ScenePath = "Assets/_Scenes/Lab04_DialogueSystem.unity"; + private const string ResponseButtonPrefabPath = "Assets/_Prefabs/DialogueResponseButton.prefab"; + + [MenuItem("Tools/Technical Game Design/Lab4/Create Dialogue System Scene")] + public static void CreateDialogueSystemScene() + { + CreateFolders(); + Font regularFont = InstallUbuntuFont(); + MaterialLibrary materials = CreateMaterials(); + ItemLibrary items = CreateItems(); + DialogueLibrary dialogue = CreateDialogue(items); + CreateGraphReadme(); + GameObject responseButtonPrefab = CreateResponseButtonPrefab(regularFont); + + Scene scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + scene.name = "Lab04_DialogueSystem"; + + BuildScene(materials, items, dialogue, responseButtonPrefab, regularFont); + + EditorSceneManager.SaveScene(scene, ScenePath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + Debug.Log("Lab04 dialogue system scene generated: " + ScenePath); + } + + private static void CreateFolders() + { + string[] paths = + { + ScriptsPath, DataPath, DialoguePath, ItemsPath, PrefabsPath, + ScenesPath, MaterialsPath, FontsPath, "Assets/Editor" + }; + + foreach (string path in paths) + Directory.CreateDirectory(path); + + AssetDatabase.Refresh(); + } + + private static Font InstallUbuntuFont() + { + string regularDestination = FontsPath + "/Ubuntu-Regular.ttf"; + string boldDestination = FontsPath + "/Ubuntu-Bold.ttf"; + + string regularSource = FindFirstExistingFile( + "/usr/share/fonts/truetype/ubuntu/Ubuntu-Regular.ttf", + "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf", + "/usr/share/fonts/TTF/Ubuntu-Regular.ttf", + "/usr/share/fonts/TTF/Ubuntu-R.ttf", + "/usr/share/fonts/Ubuntu-Regular.ttf", + "/usr/share/fonts/Ubuntu-R.ttf"); + + string boldSource = FindFirstExistingFile( + "/usr/share/fonts/truetype/ubuntu/Ubuntu-Bold.ttf", + "/usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf", + "/usr/share/fonts/TTF/Ubuntu-Bold.ttf", + "/usr/share/fonts/TTF/Ubuntu-B.ttf", + "/usr/share/fonts/Ubuntu-Bold.ttf", + "/usr/share/fonts/Ubuntu-B.ttf"); + + if (!string.IsNullOrEmpty(regularSource)) + File.Copy(regularSource, regularDestination, true); + else + Debug.LogWarning("Ubuntu font was not found in common Linux font folders. Falling back to built-in Arial."); + + if (!string.IsNullOrEmpty(boldSource)) + File.Copy(boldSource, boldDestination, true); + + if (File.Exists(regularDestination)) + AssetDatabase.ImportAsset(regularDestination); + + if (File.Exists(boldDestination)) + AssetDatabase.ImportAsset(boldDestination); + AssetDatabase.Refresh(); + + Font font = AssetDatabase.LoadAssetAtPath(regularDestination); + return font != null ? font : Resources.GetBuiltinResource("Arial.ttf"); + } + + private static string FindFirstExistingFile(params string[] candidates) + { + foreach (string candidate in candidates) + { + if (File.Exists(candidate)) + return candidate; + } + + return null; + } + + private static MaterialLibrary CreateMaterials() + { + MaterialLibrary library = new MaterialLibrary + { + ground = CreateMaterial("Ground_Mat", new Color(0.34f, 0.43f, 0.34f)), + player = CreateMaterial("Player_Mat", new Color(0.17f, 0.43f, 0.74f)), + npc = CreateMaterial("NPC_Mat", new Color(0.68f, 0.44f, 0.17f)), + pass = CreateMaterial("VillagePass_Mat", new Color(0.95f, 0.78f, 0.25f)), + prop = CreateMaterial("Props_Mat", new Color(0.42f, 0.34f, 0.25f)), + accent = CreateMaterial("Accent_Mat", new Color(0.14f, 0.62f, 0.75f)), + interactionZone = CreateMaterial("InteractionZone_Mat", new Color(0.25f, 0.75f, 1f, 0.22f), true) + }; + + return library; + } + + private static Material CreateMaterial(string name, Color color, bool transparent = false) + { + string path = MaterialsPath + "/" + name + ".mat"; + Material material = AssetDatabase.LoadAssetAtPath(path); + + if (material == null) + { + material = new Material(Shader.Find("Standard")); + AssetDatabase.CreateAsset(material, path); + } + + material.name = name; + material.color = color; + + if (transparent) + { + material.SetFloat("_Mode", 3f); + material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); + material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); + material.SetInt("_ZWrite", 0); + material.DisableKeyword("_ALPHATEST_ON"); + material.EnableKeyword("_ALPHABLEND_ON"); + material.DisableKeyword("_ALPHAPREMULTIPLY_ON"); + material.renderQueue = 3000; + } + else + { + material.SetFloat("_Mode", 0f); + material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); + material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero); + material.SetInt("_ZWrite", 1); + material.DisableKeyword("_ALPHATEST_ON"); + material.DisableKeyword("_ALPHABLEND_ON"); + material.DisableKeyword("_ALPHAPREMULTIPLY_ON"); + material.renderQueue = -1; + } + + EditorUtility.SetDirty(material); + return material; + } + + private static ItemLibrary CreateItems() + { + ItemLibrary library = new ItemLibrary + { + villagePass = LoadOrCreateAsset(ItemsPath + "/VillagePass.asset"), + oldAmulet = LoadOrCreateAsset(ItemsPath + "/OldAmulet.asset") + }; + + library.villagePass.itemId = "VillagePass"; + library.villagePass.itemName = "Пропуск"; + library.villagePass.description = "Временный документ для прохода к старосте."; + library.villagePass.itemColor = new Color(0.95f, 0.78f, 0.25f); + library.villagePass.maxStack = 1; + + library.oldAmulet.itemId = "OldAmulet"; + library.oldAmulet.itemName = "Старый амулет"; + library.oldAmulet.description = "Потёртый амулет, который ценит смотритель ворот."; + library.oldAmulet.itemColor = new Color(0.35f, 0.82f, 0.8f); + library.oldAmulet.maxStack = 1; + + EditorUtility.SetDirty(library.villagePass); + EditorUtility.SetDirty(library.oldAmulet); + AssetDatabase.SaveAssets(); + return library; + } + + private static DialogueLibrary CreateDialogue(ItemLibrary items) + { + DialogueLibrary library = new DialogueLibrary + { + start = LoadOrCreateAsset(DialoguePath + "/StartNode.asset"), + newcomer = LoadOrCreateAsset(DialoguePath + "/Node_Newcomer.asset"), + passInfo = LoadOrCreateAsset(DialoguePath + "/Node_PassInfo.asset"), + noPassWarning = LoadOrCreateAsset(DialoguePath + "/Node_NoPassWarning.asset"), + hasPass = LoadOrCreateAsset(DialoguePath + "/Node_HasPass.asset"), + work = LoadOrCreateAsset(DialoguePath + "/Node_Work.asset"), + reward = LoadOrCreateAsset(DialoguePath + "/Node_Reward.asset"), + amuletComplete = LoadOrCreateAsset(DialoguePath + "/Node_AmuletQuestComplete.asset"), + goodbye = LoadOrCreateAsset(DialoguePath + "/Node_Goodbye.asset") + }; + + SetupNode(library.start, "StartNode", "Смотритель ворот", "Приветствую, путник. Ты впервые в нашем поселении?", false, + Response("Да, я только пришёл.", library.newcomer), + Response("Я ищу работу.", library.work), + Response("Мне пора.", library.goodbye)); + + SetupNode(library.newcomer, "Node_Newcomer", "Смотритель ворот", "Тогда тебе понадобится пропуск. Без него стража не пустит тебя к старосте.", false, + Response("Где его получить?", library.passInfo), + Response("У меня уже есть пропуск.", library.hasPass, requiredItem: items.villagePass, unavailableText: "Нужен предмет: Пропуск"), + Response("Понятно.", library.goodbye)); + + SetupNode(library.passInfo, "Node_PassInfo", "Смотритель ворот", "Осмотри площадку рядом с воротами. Иногда там оставляют временные пропуска.", false, + Response("Спасибо, я поищу.", library.goodbye), + Response("Я лучше сразу пойду к старосте.", library.noPassWarning)); + + SetupNode(library.noPassWarning, "Node_NoPassWarning", "Смотритель ворот", "Без пропуска тебя не примут. Сначала найди документ.", false, + Response("Хорошо, найду пропуск.", library.goodbye)); + + SetupNode(library.hasPass, "Node_HasPass", "Смотритель ворот", "Отлично. Раз у тебя есть пропуск, можешь пройти дальше. Передай старосте, что я тебя направил.", false, + Response("Спасибо за помощь.", library.reward, rewardItem: items.oldAmulet, rewardAmount: 1, eventId: "Gatekeeper_Rewards_Amulet"), + Response("До встречи.", library.goodbye)); + + SetupNode(library.work, "Node_Work", "Смотритель ворот", "Работа есть. Принеси старый амулет, и я расскажу больше.", false, + Response("У меня есть амулет.", library.amuletComplete, requiredItem: items.oldAmulet, consumeRequiredItem: true, unavailableText: "Нужен предмет: Старый амулет", eventId: "Gatekeeper_Amulet_TurnedIn"), + Response("Я вернусь позже.", library.goodbye)); + + SetupNode(library.reward, "Node_Reward", "Смотритель ворот", "Возьми этот старый амулет. Он может пригодиться в разговоре о работе.", false, + Response("Благодарю.", library.goodbye)); + + SetupNode(library.amuletComplete, "Node_AmuletQuestComplete", "Смотритель ворот", "Ты справился. Теперь я доверяю тебе. Для следующего задания приходи позже.", false, + Response("Понял.", library.goodbye)); + + SetupNode(library.goodbye, "Node_Goodbye", "Смотритель ворот", "Удачи тебе.", true); + + AssetDatabase.SaveAssets(); + return library; + } + + private static DialogueResponse Response( + string text, + DialogueNode nextNode, + bool endsDialogue = false, + ItemData requiredItem = null, + int requiredItemAmount = 1, + string unavailableText = "", + bool hideIfRequirementsNotMet = false, + bool consumeRequiredItem = false, + ItemData rewardItem = null, + int rewardAmount = 0, + string eventId = "") + { + return new DialogueResponse + { + responseText = text, + nextNode = nextNode, + endsDialogue = endsDialogue, + requiredItem = requiredItem, + requiredItemAmount = Mathf.Max(1, requiredItemAmount), + unavailableText = unavailableText, + hideIfRequirementsNotMet = hideIfRequirementsNotMet, + consumeRequiredItem = consumeRequiredItem, + rewardItem = rewardItem, + rewardAmount = rewardAmount, + eventId = eventId + }; + } + + private static void SetupNode(DialogueNode node, string id, string speaker, string line, bool isEnd, params DialogueResponse[] responses) + { + node.nodeId = id; + node.speakerName = speaker; + node.npcLine = line; + node.isEndNode = isEnd; + node.responses = responses; + node.editorNote = "Generated by Lab4SceneBuilder."; + EditorUtility.SetDirty(node); + } + + private static T LoadOrCreateAsset(string path) where T : ScriptableObject + { + T asset = AssetDatabase.LoadAssetAtPath(path); + if (asset != null) + return asset; + + asset = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(asset, path); + return asset; + } + + private static void CreateGraphReadme() + { + const string text = + "StartNode\n" + + "├─ Да, я только пришёл → Node_Newcomer\n" + + "│ ├─ Где его получить? → Node_PassInfo → Node_Goodbye\n" + + "│ ├─ У меня уже есть пропуск [requires VillagePass] → Node_HasPass → Node_Reward [gives OldAmulet] → Node_Goodbye\n" + + "│ └─ Понятно → Node_Goodbye\n" + + "├─ Я ищу работу → Node_Work\n" + + "│ ├─ У меня есть амулет [requires OldAmulet, consumes] → Node_AmuletQuestComplete → Node_Goodbye\n" + + "│ └─ Я вернусь позже → Node_Goodbye\n" + + "└─ Мне пора → Node_Goodbye\n"; + + File.WriteAllText(DialoguePath + "/DialogueGraph_Readme.txt", text); + } + + private static GameObject CreateResponseButtonPrefab(Font font) + { + GameObject root = new GameObject("DialogueResponseButton", typeof(RectTransform), typeof(Image), typeof(Button), typeof(LayoutElement), typeof(Outline)); + RectTransform rect = root.GetComponent(); + rect.sizeDelta = new Vector2(0f, 38f); + + Image image = root.GetComponent(); + image.color = new Color(0.095f, 0.12f, 0.15f, 0.98f); + + Outline outline = root.GetComponent(); + outline.effectColor = new Color(0.36f, 0.48f, 0.54f, 0.28f); + outline.effectDistance = new Vector2(1f, -1f); + + Button button = root.GetComponent