first commit

This commit is contained in:
2026-06-04 23:22:13 +03:00
commit d329f501be
310 changed files with 36395 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using OTGIntegrated.Items;
using UnityEngine;
namespace OTGIntegrated.Quests
{
[Serializable]
public sealed class QuestRewardItem
{
public ItemData item;
[Min(1)] public int amount = 1;
}
[CreateAssetMenu(fileName = "QuestData", menuName = "OTG Integrated/Quest Data")]
public sealed class QuestData : ScriptableObject
{
public string questId;
public string displayName;
[TextArea(3, 7)] public string description;
public string giverNpcId;
public string turnInNpcId;
public List<QuestGoal> goals = new List<QuestGoal>();
[Min(0)] public int rewardExperience;
[Min(0)] public int rewardTalentPoints;
public List<QuestRewardItem> rewardItems = new List<QuestRewardItem>();
private void OnValidate()
{
if (string.IsNullOrWhiteSpace(questId))
{
questId = name;
}
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = questId;
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 12fa02eac54ea59fca4b6c5a2c604992
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+64
View File
@@ -0,0 +1,64 @@
using System;
using OTGIntegrated.Items;
using UnityEngine;
namespace OTGIntegrated.Quests
{
public enum QuestGoalType
{
CollectItem,
KillEnemy,
TalkToNPC,
CraftItem
}
public enum QuestStatus
{
Active,
ReadyToComplete,
Completed
}
[Serializable]
public sealed class QuestGoal
{
public QuestGoalType goalType;
public ItemData targetItem;
public string enemyId;
public string npcId;
[Min(1)] public int requiredAmount = 1;
public string GetTargetKey()
{
switch (goalType)
{
case QuestGoalType.CollectItem:
case QuestGoalType.CraftItem:
return targetItem != null ? targetItem.itemId : string.Empty;
case QuestGoalType.KillEnemy:
return enemyId;
case QuestGoalType.TalkToNPC:
return npcId;
default:
return string.Empty;
}
}
public string GetDisplayName()
{
switch (goalType)
{
case QuestGoalType.CollectItem:
return targetItem != null ? targetItem.displayName : "Предмет";
case QuestGoalType.CraftItem:
return targetItem != null ? $"Создать {targetItem.displayName}" : "Создать предмет";
case QuestGoalType.KillEnemy:
return string.IsNullOrWhiteSpace(enemyId) ? "Враг" : enemyId;
case QuestGoalType.TalkToNPC:
return string.IsNullOrWhiteSpace(npcId) ? "NPC" : npcId;
default:
return "Цель";
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 67df6628261903cac9131cc30928ae5b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+172
View File
@@ -0,0 +1,172 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace OTGIntegrated.Quests
{
public sealed class QuestLogUI : MonoBehaviour
{
public GameObject panelRoot;
public Transform activeListRoot;
public Transform completedListRoot;
public Text titleText;
public Text detailText;
public Button closeButton;
public Font uiFont;
private QuestManager questManager;
private QuestState selectedState;
private readonly List<GameObject> spawnedButtons = new List<GameObject>();
public void Initialize(QuestManager manager)
{
questManager = manager;
if (questManager != null)
{
questManager.OnQuestsChanged -= Refresh;
questManager.OnQuestsChanged += Refresh;
}
if (closeButton != null)
{
closeButton.onClick.RemoveAllListeners();
closeButton.onClick.AddListener(() => FindObjectOfType<OTGIntegrated.UI.UIManager>()?.CloseCurrentWindow());
}
Refresh();
}
private void OnEnable()
{
Refresh();
}
private void OnDestroy()
{
if (questManager != null)
{
questManager.OnQuestsChanged -= Refresh;
}
}
public void Refresh()
{
ClearButtons();
if (titleText != null)
{
titleText.text = "Журнал квестов";
}
if (questManager == null)
{
SetDetails(null);
return;
}
if (selectedState == null && questManager.ActiveQuests.Count > 0)
{
selectedState = questManager.ActiveQuests[0];
}
CreateButtons(activeListRoot, questManager.ActiveQuests);
CreateButtons(completedListRoot, questManager.CompletedQuests);
SetDetails(selectedState);
}
private void CreateButtons(Transform parent, IReadOnlyList<QuestState> states)
{
if (parent == null || states == null)
{
return;
}
for (int i = 0; i < states.Count; i++)
{
QuestState state = states[i];
if (state == null || state.questData == null)
{
continue;
}
GameObject root = new GameObject(state.questData.displayName, typeof(RectTransform), typeof(Image), typeof(Button));
root.transform.SetParent(parent, false);
spawnedButtons.Add(root);
root.GetComponent<RectTransform>().sizeDelta = new Vector2(320f, 44f);
Image image = root.GetComponent<Image>();
image.color = GetStatusColor(state.status);
Text label = CreateText(root.transform, "Text", state.questData.displayName, 16, TextAnchor.MiddleLeft);
label.rectTransform.anchorMin = Vector2.zero;
label.rectTransform.anchorMax = Vector2.one;
label.rectTransform.offsetMin = new Vector2(12f, 0f);
label.rectTransform.offsetMax = new Vector2(-8f, 0f);
Button button = root.GetComponent<Button>();
button.onClick.AddListener(() =>
{
selectedState = state;
SetDetails(selectedState);
});
}
}
private void SetDetails(QuestState state)
{
if (detailText == null)
{
return;
}
if (state == null || state.questData == null)
{
detailText.text = "Нет выбранного квеста.";
return;
}
QuestData quest = state.questData;
detailText.text =
$"{quest.displayName}\n\n{quest.description}\n\nСтатус: {state.status}\n\n{questManager.GetStateProgressText(state)}\n\nНаграды:\nОпыт: {quest.rewardExperience}\nОчки талантов: {quest.rewardTalentPoints}";
}
private Color GetStatusColor(QuestStatus status)
{
switch (status)
{
case QuestStatus.ReadyToComplete:
return new Color(0.16f, 0.42f, 0.22f, 0.95f);
case QuestStatus.Completed:
return new Color(0.16f, 0.2f, 0.34f, 0.82f);
default:
return new Color(0.46f, 0.34f, 0.12f, 0.92f);
}
}
private Text CreateText(Transform parent, string name, string value, int fontSize, TextAnchor anchor)
{
GameObject textObject = new GameObject(name, typeof(RectTransform), typeof(Text));
textObject.transform.SetParent(parent, false);
Text text = textObject.GetComponent<Text>();
text.font = uiFont != null ? uiFont : Resources.GetBuiltinResource<Font>("Arial.ttf");
text.fontSize = fontSize;
text.alignment = anchor;
text.color = new Color(0.94f, 0.93f, 0.88f, 1f);
text.text = value;
return text;
}
private void ClearButtons()
{
for (int i = 0; i < spawnedButtons.Count; i++)
{
if (spawnedButtons[i] != null)
{
Destroy(spawnedButtons[i]);
}
}
spawnedButtons.Clear();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 61452d00fee61482aa5fb3d1fda042cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+498
View File
@@ -0,0 +1,498 @@
using System;
using System.Collections.Generic;
using OTGIntegrated.Core;
using OTGIntegrated.Items;
using OTGIntegrated.Stats;
using OTGIntegrated.Talents;
using UnityEngine;
namespace OTGIntegrated.Quests
{
[Serializable]
public sealed class QuestState
{
public QuestData questData;
public QuestStatus status;
public List<int> progress = new List<int>();
public QuestState(QuestData quest)
{
questData = quest;
status = QuestStatus.Active;
int count = quest != null && quest.goals != null ? quest.goals.Count : 0;
for (int i = 0; i < count; i++)
{
progress.Add(0);
}
}
public int GetProgress(int goalIndex)
{
return goalIndex >= 0 && goalIndex < progress.Count ? progress[goalIndex] : 0;
}
public void SetProgress(int goalIndex, int amount)
{
if (questData == null || questData.goals == null || goalIndex < 0 || goalIndex >= questData.goals.Count)
{
return;
}
while (progress.Count < questData.goals.Count)
{
progress.Add(0);
}
int required = Mathf.Max(1, questData.goals[goalIndex].requiredAmount);
progress[goalIndex] = Mathf.Clamp(amount, 0, required);
}
public void AddProgress(int goalIndex, int amount)
{
SetProgress(goalIndex, GetProgress(goalIndex) + Mathf.Max(0, amount));
}
public bool IsGoalComplete(int goalIndex)
{
if (questData == null || questData.goals == null || goalIndex < 0 || goalIndex >= questData.goals.Count)
{
return false;
}
return GetProgress(goalIndex) >= Mathf.Max(1, questData.goals[goalIndex].requiredAmount);
}
public bool AreAllGoalsComplete()
{
if (questData == null || questData.goals == null || questData.goals.Count == 0)
{
return false;
}
for (int i = 0; i < questData.goals.Count; i++)
{
if (!IsGoalComplete(i))
{
return false;
}
}
return true;
}
}
[Serializable]
public sealed class QuestSaveEntry
{
public string questId;
public QuestStatus status;
public List<int> progress = new List<int>();
}
public sealed class QuestManager : MonoBehaviour
{
public List<QuestData> allQuests = new List<QuestData>();
[SerializeField] private List<QuestState> activeQuests = new List<QuestState>();
[SerializeField] private List<QuestState> completedQuests = new List<QuestState>();
private OTGIntegrated.Inventory.Inventory inventory;
private LevelSystem levelSystem;
private TalentManager talentManager;
private readonly Dictionary<string, QuestData> questLookup = new Dictionary<string, QuestData>(StringComparer.Ordinal);
public event Action OnQuestsChanged;
public IReadOnlyList<QuestState> ActiveQuests => activeQuests;
public IReadOnlyList<QuestState> CompletedQuests => completedQuests;
public void Initialize(OTGIntegrated.Inventory.Inventory inventory, LevelSystem levelSystem, TalentManager talentManager)
{
this.inventory = inventory;
this.levelSystem = levelSystem;
this.talentManager = talentManager;
BuildLookup();
OnQuestsChanged?.Invoke();
}
private void OnEnable()
{
GameEvents.OnItemAdded += HandleItemAdded;
GameEvents.OnEnemyKilled += HandleEnemyKilled;
}
private void OnDisable()
{
GameEvents.OnItemAdded -= HandleItemAdded;
GameEvents.OnEnemyKilled -= HandleEnemyKilled;
}
public bool StartQuest(QuestData quest)
{
if (quest == null || IsQuestActive(quest) || IsQuestCompleted(quest))
{
return false;
}
QuestState state = new QuestState(quest);
activeQuests.Add(state);
SynchronizeInventoryGoals(state);
UpdateReadyState(state);
GameEvents.RaiseQuestStarted(quest);
GameEvents.RaiseNotification($"Квест получен: {quest.displayName}");
OnQuestsChanged?.Invoke();
return true;
}
public bool TryCompleteQuest(QuestData quest, string npcId)
{
QuestState state = GetActiveState(quest);
if (state == null)
{
return false;
}
if (!string.IsNullOrWhiteSpace(quest.turnInNpcId) && quest.turnInNpcId != npcId)
{
return false;
}
if (state.status != QuestStatus.ReadyToComplete && !state.AreAllGoalsComplete())
{
GameEvents.RaiseNotification("Цели квеста ещё не выполнены");
return false;
}
if (!ConsumeCollectGoals(quest))
{
GameEvents.RaiseNotification("Не хватает предметов для сдачи квеста");
return false;
}
state.status = QuestStatus.Completed;
activeQuests.Remove(state);
completedQuests.Add(state);
if (quest.rewardExperience > 0 && levelSystem != null)
{
levelSystem.AddExperience(quest.rewardExperience);
}
if (quest.rewardTalentPoints > 0 && talentManager != null)
{
talentManager.AddTalentPoints(quest.rewardTalentPoints);
}
if (quest.rewardItems != null && inventory != null)
{
for (int i = 0; i < quest.rewardItems.Count; i++)
{
QuestRewardItem reward = quest.rewardItems[i];
if (reward != null && reward.item != null && reward.amount > 0)
{
inventory.AddItem(reward.item, reward.amount);
}
}
}
GameEvents.RaiseQuestCompleted(quest);
GameEvents.RaiseNotification($"Квест выполнен: {quest.displayName}");
OnQuestsChanged?.Invoke();
return true;
}
public void NotifyNpcTalked(string npcId)
{
if (!string.IsNullOrWhiteSpace(npcId))
{
UpdateProgress(QuestGoalType.TalkToNPC, npcId, 1);
}
}
public bool IsQuestActive(QuestData quest)
{
return GetActiveState(quest) != null;
}
public bool IsQuestCompleted(QuestData quest)
{
return quest != null && completedQuests.Exists(state => state.questData == quest);
}
public bool IsQuestReady(QuestData quest)
{
QuestState state = GetActiveState(quest);
return state != null && state.status == QuestStatus.ReadyToComplete;
}
public QuestState GetActiveState(QuestData quest)
{
return quest == null ? null : activeQuests.Find(state => state.questData == quest);
}
public QuestData GetQuestById(string questId)
{
BuildLookup();
if (string.IsNullOrWhiteSpace(questId))
{
return null;
}
questLookup.TryGetValue(questId, out QuestData quest);
return quest;
}
public string GetPrimaryTrackerText()
{
QuestState state = activeQuests.Count > 0 ? activeQuests[0] : null;
if (state == null || state.questData == null)
{
return "Нет активных квестов";
}
return $"{state.questData.displayName}\n{GetStateProgressText(state)}";
}
public string GetStateProgressText(QuestState state)
{
if (state == null || state.questData == null || state.questData.goals == null)
{
return string.Empty;
}
System.Text.StringBuilder builder = new System.Text.StringBuilder();
for (int i = 0; i < state.questData.goals.Count; i++)
{
QuestGoal goal = state.questData.goals[i];
if (goal == null)
{
continue;
}
builder.Append(goal.GetDisplayName());
builder.Append(": ");
builder.Append(state.GetProgress(i));
builder.Append(" / ");
builder.Append(goal.requiredAmount);
if (i < state.questData.goals.Count - 1)
{
builder.AppendLine();
}
}
if (state.status == QuestStatus.ReadyToComplete)
{
builder.AppendLine();
builder.Append("Вернитесь к NPC");
}
return builder.ToString();
}
public List<QuestSaveEntry> SaveData()
{
List<QuestSaveEntry> entries = new List<QuestSaveEntry>();
AppendSaveEntries(activeQuests, entries);
AppendSaveEntries(completedQuests, entries);
return entries;
}
public void LoadData(List<QuestSaveEntry> entries)
{
BuildLookup();
activeQuests.Clear();
completedQuests.Clear();
if (entries != null)
{
for (int i = 0; i < entries.Count; i++)
{
QuestSaveEntry entry = entries[i];
if (entry == null || string.IsNullOrWhiteSpace(entry.questId))
{
continue;
}
QuestData quest = GetQuestById(entry.questId);
if (quest == null)
{
continue;
}
QuestState state = new QuestState(quest);
state.status = entry.status;
for (int p = 0; entry.progress != null && p < entry.progress.Count && p < state.progress.Count; p++)
{
state.SetProgress(p, entry.progress[p]);
}
if (state.status == QuestStatus.Completed)
{
completedQuests.Add(state);
}
else
{
activeQuests.Add(state);
}
}
}
OnQuestsChanged?.Invoke();
}
private void HandleItemAdded(ItemData item, int amount)
{
if (item == null || amount <= 0)
{
return;
}
UpdateProgress(QuestGoalType.CollectItem, item.itemId, amount);
UpdateProgress(QuestGoalType.CraftItem, item.itemId, amount);
}
private void HandleEnemyKilled(string enemyId)
{
if (!string.IsNullOrWhiteSpace(enemyId))
{
UpdateProgress(QuestGoalType.KillEnemy, enemyId, 1);
}
}
private void UpdateProgress(QuestGoalType goalType, string targetKey, int amount)
{
if (string.IsNullOrWhiteSpace(targetKey) || amount <= 0)
{
return;
}
bool changed = false;
for (int i = 0; i < activeQuests.Count; i++)
{
QuestState state = activeQuests[i];
QuestData quest = state.questData;
if (state.status == QuestStatus.ReadyToComplete || quest == null || quest.goals == null)
{
continue;
}
for (int goalIndex = 0; goalIndex < quest.goals.Count; goalIndex++)
{
QuestGoal goal = quest.goals[goalIndex];
if (goal == null || goal.goalType != goalType || goal.GetTargetKey() != targetKey || state.IsGoalComplete(goalIndex))
{
continue;
}
state.AddProgress(goalIndex, amount);
changed = true;
GameEvents.RaiseQuestProgressChanged(quest);
GameEvents.RaiseNotification($"{quest.displayName}: {goal.GetDisplayName()} {state.GetProgress(goalIndex)}/{goal.requiredAmount}");
}
if (UpdateReadyState(state))
{
changed = true;
}
}
if (changed)
{
OnQuestsChanged?.Invoke();
}
}
private bool UpdateReadyState(QuestState state)
{
if (state == null || state.status != QuestStatus.Active || !state.AreAllGoalsComplete())
{
return false;
}
state.status = QuestStatus.ReadyToComplete;
GameEvents.RaiseQuestReadyToComplete(state.questData);
GameEvents.RaiseNotification($"Квест готов к сдаче: {state.questData.displayName}");
return true;
}
private void SynchronizeInventoryGoals(QuestState state)
{
if (state == null || state.questData == null || state.questData.goals == null || inventory == null)
{
return;
}
for (int i = 0; i < state.questData.goals.Count; i++)
{
QuestGoal goal = state.questData.goals[i];
if (goal != null && goal.goalType == QuestGoalType.CollectItem && goal.targetItem != null)
{
state.SetProgress(i, inventory.GetAmount(goal.targetItem));
}
}
}
private bool ConsumeCollectGoals(QuestData quest)
{
if (quest == null || quest.goals == null || inventory == null)
{
return true;
}
for (int i = 0; i < quest.goals.Count; i++)
{
QuestGoal goal = quest.goals[i];
if (goal != null && goal.goalType == QuestGoalType.CollectItem && goal.targetItem != null)
{
if (!inventory.HasItem(goal.targetItem, goal.requiredAmount))
{
return false;
}
}
}
for (int i = 0; i < quest.goals.Count; i++)
{
QuestGoal goal = quest.goals[i];
if (goal != null && goal.goalType == QuestGoalType.CollectItem && goal.targetItem != null)
{
inventory.RemoveItem(goal.targetItem, goal.requiredAmount);
}
}
return true;
}
private void BuildLookup()
{
questLookup.Clear();
for (int i = 0; i < allQuests.Count; i++)
{
QuestData quest = allQuests[i];
if (quest != null && !string.IsNullOrWhiteSpace(quest.questId))
{
questLookup[quest.questId] = quest;
}
}
}
private void AppendSaveEntries(List<QuestState> states, List<QuestSaveEntry> entries)
{
for (int i = 0; i < states.Count; i++)
{
QuestState state = states[i];
if (state == null || state.questData == null)
{
continue;
}
entries.Add(new QuestSaveEntry
{
questId = state.questData.questId,
status = state.status,
progress = new List<int>(state.progress)
});
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9a384ce688ea1dd4db6978f80a709235
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: