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
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0c9ca1efd428be9728a91e7626205769
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+36
View File
@@ -0,0 +1,36 @@
using UnityEngine;
namespace OTGIntegrated.Abilities
{
[CreateAssetMenu(fileName = "AbilityData", menuName = "OTG Integrated/Ability Data")]
public sealed class AbilityData : ScriptableObject
{
public string abilityId;
public string displayName;
[TextArea(2, 5)] public string description;
public float manaCost = 10f;
public float cooldown = 2f;
public float damage = 10f;
public float range = 8f;
public GameObject effectPrefab;
public Sprite icon;
private void OnValidate()
{
if (string.IsNullOrWhiteSpace(abilityId))
{
abilityId = name;
}
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = abilityId;
}
manaCost = Mathf.Max(0f, manaCost);
cooldown = Mathf.Max(0f, cooldown);
damage = Mathf.Max(0f, damage);
range = Mathf.Max(0f, range);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ad890eb3c985db91b32d80f01945dec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
using OTGIntegrated.Stats;
using UnityEngine;
namespace OTGIntegrated.Abilities
{
public readonly struct AbilityContext
{
public readonly AbilityData Ability;
public readonly Transform Caster;
public readonly PlayerStats Stats;
public readonly Vector3 Origin;
public readonly Vector3 Forward;
public AbilityContext(AbilityData ability, Transform caster, PlayerStats stats, Vector3 origin, Vector3 forward)
{
Ability = ability;
Caster = caster;
Stats = stats;
Origin = origin;
Forward = forward.sqrMagnitude > 0.001f ? forward.normalized : Vector3.forward;
}
}
public abstract class AbilityEffect : MonoBehaviour
{
public abstract void Activate(AbilityContext context);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac1934c301c9feb07b0a0e29236e916f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+181
View File
@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using OTGIntegrated.Core;
using OTGIntegrated.Player;
using OTGIntegrated.Stats;
using UnityEngine;
namespace OTGIntegrated.Abilities
{
public sealed class AbilityManager : MonoBehaviour
{
public List<AbilityData> learnedAbilities = new List<AbilityData>();
public Transform castOrigin;
private readonly List<float> cooldownRemaining = new List<float>();
private PlayerStats playerStats;
private PlayerController playerController;
public event Action OnAbilitiesChanged;
public IReadOnlyList<AbilityData> LearnedAbilities => learnedAbilities;
public void Initialize(PlayerStats stats)
{
playerStats = stats != null ? stats : GetComponent<PlayerStats>();
playerController = GetComponent<PlayerController>();
if (castOrigin == null)
{
castOrigin = transform;
}
EnsureCooldownSlots();
OnAbilitiesChanged?.Invoke();
}
private void Awake()
{
if (playerStats == null)
{
playerStats = GetComponent<PlayerStats>();
}
playerController = GetComponent<PlayerController>();
EnsureCooldownSlots();
}
private void Update()
{
TickCooldowns();
if (playerController != null && !playerController.InputEnabled)
{
return;
}
if (Input.GetKeyDown(KeyCode.Alpha1)) TryUseAbility(0);
if (Input.GetKeyDown(KeyCode.Alpha2)) TryUseAbility(1);
if (Input.GetKeyDown(KeyCode.Alpha3)) TryUseAbility(2);
}
public bool TryUseAbility(int index)
{
EnsureCooldownSlots();
if (index < 0 || index >= learnedAbilities.Count)
{
return false;
}
AbilityData ability = learnedAbilities[index];
if (ability == null || playerStats == null)
{
return false;
}
if (cooldownRemaining[index] > 0f)
{
GameEvents.RaiseNotification("Способность на перезарядке");
return false;
}
if (!playerStats.SpendMana(ability.manaCost))
{
return false;
}
if (ability.effectPrefab == null)
{
playerStats.RestoreMana(ability.manaCost);
Debug.LogWarning($"Ability has no effect prefab: {ability.displayName}", ability);
return false;
}
Vector3 forward = GetCastForward();
Vector3 origin = castOrigin != null ? castOrigin.position : transform.position + Vector3.up;
GameObject effectObject = Instantiate(ability.effectPrefab, origin, Quaternion.LookRotation(forward, Vector3.up));
AbilityEffect effect = effectObject.GetComponent<AbilityEffect>();
if (effect == null)
{
playerStats.RestoreMana(ability.manaCost);
Destroy(effectObject);
Debug.LogWarning($"Ability effect prefab misses AbilityEffect: {ability.effectPrefab.name}", ability.effectPrefab);
return false;
}
effect.Activate(new AbilityContext(ability, transform, playerStats, origin, forward));
cooldownRemaining[index] = GetEffectiveCooldown(ability);
GameEvents.RaiseAbilityUsed(ability);
GameEvents.RaiseNotification($"Способность: {ability.displayName}");
OnAbilitiesChanged?.Invoke();
return true;
}
public float GetCooldownRemaining(int index)
{
EnsureCooldownSlots();
return index >= 0 && index < cooldownRemaining.Count ? cooldownRemaining[index] : 0f;
}
public float GetCooldownNormalized(int index)
{
if (index < 0 || index >= learnedAbilities.Count || learnedAbilities[index] == null)
{
return 0f;
}
float cooldown = GetEffectiveCooldown(learnedAbilities[index]);
return cooldown <= 0f ? 0f : Mathf.Clamp01(GetCooldownRemaining(index) / cooldown);
}
private float GetEffectiveCooldown(AbilityData ability)
{
float reduction = playerStats != null ? playerStats.CooldownReduction : 0f;
return Mathf.Max(0f, ability.cooldown * (1f - Mathf.Clamp(reduction, 0f, 0.9f)));
}
private void TickCooldowns()
{
EnsureCooldownSlots();
bool changed = false;
for (int i = 0; i < cooldownRemaining.Count; i++)
{
if (cooldownRemaining[i] <= 0f)
{
continue;
}
cooldownRemaining[i] = Mathf.Max(0f, cooldownRemaining[i] - Time.deltaTime);
changed = true;
}
if (changed)
{
OnAbilitiesChanged?.Invoke();
}
}
private void EnsureCooldownSlots()
{
while (cooldownRemaining.Count < learnedAbilities.Count)
{
cooldownRemaining.Add(0f);
}
while (cooldownRemaining.Count > learnedAbilities.Count)
{
cooldownRemaining.RemoveAt(cooldownRemaining.Count - 1);
}
}
private Vector3 GetCastForward()
{
if (playerController != null)
{
return playerController.GetAimForward();
}
Vector3 forward = transform.forward;
forward.y = 0f;
return forward.sqrMagnitude > 0.001f ? forward.normalized : Vector3.forward;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af4f4f6d82dfc81a6b41ef63d2c4fef0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+43
View File
@@ -0,0 +1,43 @@
using UnityEngine;
namespace OTGIntegrated.Abilities
{
public sealed class BlinkEffect : AbilityEffect
{
public LayerMask obstacleMask = ~0;
public float collisionRadius = 0.45f;
public override void Activate(AbilityContext context)
{
if (context.Caster == null)
{
Destroy(gameObject);
return;
}
Vector3 origin = context.Caster.position + Vector3.up * 0.8f;
Vector3 direction = context.Forward;
float distance = Mathf.Max(0f, context.Ability.range);
if (Physics.SphereCast(origin, collisionRadius, direction, out RaycastHit hit, distance, obstacleMask, QueryTriggerInteraction.Ignore))
{
distance = Mathf.Max(0f, hit.distance - collisionRadius);
}
Vector3 target = context.Caster.position + direction * distance;
CharacterController controller = context.Caster.GetComponent<CharacterController>();
if (controller != null)
{
controller.enabled = false;
context.Caster.position = target;
controller.enabled = true;
}
else
{
context.Caster.position = target;
}
Destroy(gameObject, 0.15f);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e0a3dce724dee68bd9503fe7a80a80b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,81 @@
using OTGIntegrated.Combat;
using UnityEngine;
namespace OTGIntegrated.Abilities
{
[RequireComponent(typeof(Collider))]
public sealed class FireballEffect : AbilityEffect
{
public float speed = 16f;
public float radius = 0.35f;
public LayerMask hitMask = ~0;
private AbilityContext context;
private Vector3 direction;
private float remainingLife;
private bool active;
private void Awake()
{
Collider ownCollider = GetComponent<Collider>();
ownCollider.isTrigger = true;
}
public override void Activate(AbilityContext context)
{
this.context = context;
direction = context.Forward;
remainingLife = Mathf.Max(0.5f, context.Ability.range / Mathf.Max(1f, speed));
active = true;
transform.position = context.Origin + direction * 0.9f;
transform.rotation = Quaternion.LookRotation(direction, Vector3.up);
}
private void Update()
{
if (!active)
{
return;
}
float step = speed * Time.deltaTime;
if (Physics.SphereCast(transform.position, radius, direction, out RaycastHit hit, step, hitMask, QueryTriggerInteraction.Ignore))
{
ApplyHit(hit.collider);
return;
}
transform.position += direction * step;
remainingLife -= Time.deltaTime;
if (remainingLife <= 0f)
{
Destroy(gameObject);
}
}
private void OnTriggerEnter(Collider other)
{
if (active)
{
ApplyHit(other);
}
}
private void ApplyHit(Collider hitCollider)
{
if (hitCollider == null || context.Caster == null || hitCollider.transform == context.Caster || hitCollider.GetComponentInParent<AbilityEffect>() != null)
{
return;
}
Damageable damageable = hitCollider.GetComponentInParent<Damageable>();
if (damageable != null && damageable.IsAlive)
{
float finalDamage = context.Ability.damage + (context.Stats != null ? context.Stats.Damage : 0f);
damageable.TakeDamage(finalDamage, context.Caster.gameObject);
}
Destroy(gameObject);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fa6f20c109c09c3d99226f53d98d4bb7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+19
View File
@@ -0,0 +1,19 @@
using OTGIntegrated.Combat;
using UnityEngine;
namespace OTGIntegrated.Abilities
{
public sealed class HealEffect : AbilityEffect
{
public override void Activate(AbilityContext context)
{
if (context.Stats != null)
{
context.Stats.Heal(context.Ability.damage);
FloatingDamage.Spawn(context.Stats.transform.position + Vector3.up * 1.7f, Mathf.RoundToInt(context.Ability.damage), new Color(0.35f, 1f, 0.45f, 1f));
}
Destroy(gameObject, 0.1f);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: acee80077d1ce3be382de5f654f93f2b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f694ab5867017989da700c10769b7ef2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+65
View File
@@ -0,0 +1,65 @@
using System;
using UnityEngine;
namespace OTGIntegrated.Combat
{
public class Damageable : MonoBehaviour
{
public string displayName = "Target";
[Min(1f)] public float maxHealth = 50f;
[SerializeField] protected float currentHealth = 50f;
public Transform floatingDamageAnchor;
public event Action<float> OnDamageTaken;
public event Action OnDeath;
public float CurrentHealth => currentHealth;
public float MaxHealth => maxHealth;
public bool IsAlive => currentHealth > 0f;
protected virtual void Awake()
{
maxHealth = Mathf.Max(1f, maxHealth);
if (currentHealth <= 0f)
{
currentHealth = maxHealth;
}
currentHealth = Mathf.Clamp(currentHealth, 0f, maxHealth);
}
public virtual void TakeDamage(float amount, GameObject source = null)
{
if (!IsAlive || amount <= 0f)
{
return;
}
currentHealth = Mathf.Clamp(currentHealth - amount, 0f, maxHealth);
OnDamageTaken?.Invoke(amount);
Vector3 anchor = floatingDamageAnchor != null ? floatingDamageAnchor.position : transform.position + Vector3.up * 1.6f;
FloatingDamage.Spawn(anchor, Mathf.RoundToInt(amount), new Color(1f, 0.46f, 0.24f, 1f));
if (currentHealth <= 0f)
{
HandleDeath(source);
}
}
public virtual void Heal(float amount)
{
if (amount <= 0f || !IsAlive)
{
return;
}
currentHealth = Mathf.Clamp(currentHealth + amount, 0f, maxHealth);
}
protected virtual void HandleDeath(GameObject source)
{
OnDeath?.Invoke();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5cc71487b17c578579e59895008f7bb4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+125
View File
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using OTGIntegrated.Core;
using OTGIntegrated.Items;
using OTGIntegrated.Stats;
using UnityEngine;
namespace OTGIntegrated.Combat
{
[Serializable]
public sealed class EnemyDrop
{
public ItemData item;
[Min(1)] public int amount = 1;
}
public sealed class Enemy : Damageable
{
public string enemyId = "Goblin";
public float damage = 5f;
public int expReward = 20;
public List<EnemyDrop> dropItems = new List<EnemyDrop>();
public float aggroRange = 8f;
public float attackRange = 1.8f;
public float attackCooldown = 1.4f;
public float moveSpeed = 2.2f;
private Transform player;
private PlayerStats playerStats;
private float nextAttackTime;
private bool dead;
protected override void Awake()
{
base.Awake();
currentHealth = maxHealth;
}
private void Start()
{
if (GameManager.Instance != null && GameManager.Instance.playerStats != null)
{
playerStats = GameManager.Instance.playerStats;
player = playerStats.transform;
}
else
{
PlayerStats stats = FindObjectOfType<PlayerStats>();
if (stats != null)
{
playerStats = stats;
player = stats.transform;
}
}
}
private void Update()
{
if (!IsAlive || player == null)
{
return;
}
Vector3 toPlayer = player.position - transform.position;
toPlayer.y = 0f;
float distance = toPlayer.magnitude;
if (distance <= aggroRange && distance > attackRange)
{
Vector3 direction = toPlayer.normalized;
transform.position += direction * moveSpeed * Time.deltaTime;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), 8f * Time.deltaTime);
}
if (distance <= attackRange && Time.time >= nextAttackTime)
{
nextAttackTime = Time.time + attackCooldown;
if (playerStats != null)
{
playerStats.TakeDamage(damage);
GameEvents.RaiseNotification($"{displayName} наносит {Mathf.RoundToInt(damage)} урона");
}
}
}
protected override void HandleDeath(GameObject source)
{
if (dead)
{
return;
}
dead = true;
DropRewards();
if (GameManager.Instance != null && GameManager.Instance.levelSystem != null && expReward > 0)
{
GameManager.Instance.levelSystem.AddExperience(expReward);
}
GameEvents.RaiseEnemyKilled(enemyId);
GameEvents.RaiseNotification($"{displayName} повержен");
base.HandleDeath(source);
Destroy(gameObject, 0.15f);
}
private void DropRewards()
{
OTGIntegrated.Inventory.Inventory inventory = GameManager.Instance != null ? GameManager.Instance.inventory : null;
if (inventory == null || dropItems == null)
{
return;
}
for (int i = 0; i < dropItems.Count; i++)
{
EnemyDrop drop = dropItems[i];
if (drop != null && drop.item != null && drop.amount > 0)
{
inventory.AddItem(drop.item, drop.amount);
}
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e203683fe758dec587d4bddb4347392
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+55
View File
@@ -0,0 +1,55 @@
using UnityEngine;
namespace OTGIntegrated.Combat
{
public sealed class FloatingDamage : MonoBehaviour
{
[SerializeField] private float lifetime = 1.1f;
[SerializeField] private float riseSpeed = 1.35f;
private TextMesh textMesh;
private float age;
public static void Spawn(Vector3 position, int amount, Color color)
{
GameObject root = new GameObject("FloatingDamage");
root.transform.position = position;
FloatingDamage floatingDamage = root.AddComponent<FloatingDamage>();
floatingDamage.Configure(amount.ToString(), color);
}
private void Configure(string text, Color color)
{
textMesh = gameObject.AddComponent<TextMesh>();
textMesh.text = text;
textMesh.anchor = TextAnchor.MiddleCenter;
textMesh.alignment = TextAlignment.Center;
textMesh.characterSize = 0.22f;
textMesh.fontSize = 48;
textMesh.color = color;
}
private void Update()
{
age += Time.deltaTime;
transform.position += Vector3.up * riseSpeed * Time.deltaTime;
if (Camera.main != null)
{
transform.rotation = Quaternion.LookRotation(transform.position - Camera.main.transform.position, Vector3.up);
}
if (textMesh != null)
{
Color color = textMesh.color;
color.a = Mathf.Clamp01(1f - age / lifetime);
textMesh.color = color;
}
if (age >= lifetime)
{
Destroy(gameObject);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0b803690c5148050782bf7e789b81cde
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 81836fcfb7483bbb086ffd8384723a9d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+44
View File
@@ -0,0 +1,44 @@
using System;
using OTGIntegrated.Abilities;
using OTGIntegrated.Items;
using OTGIntegrated.Quests;
using OTGIntegrated.Talents;
using OTGIntegrated.Weapons;
namespace OTGIntegrated.Core
{
public static class GameEvents
{
public static event Action<ItemData, int> OnItemAdded;
public static event Action<ItemData, int> OnItemRemoved;
public static event Action<string> OnEnemyKilled;
public static event Action<QuestData> OnQuestStarted;
public static event Action<QuestData> OnQuestProgressChanged;
public static event Action<QuestData> OnQuestReadyToComplete;
public static event Action<QuestData> OnQuestCompleted;
public static event Action<int> OnExperienceGained;
public static event Action<int> OnLevelUp;
public static event Action<int> OnTalentPointChanged;
public static event Action<TalentData> OnTalentLearned;
public static event Action<AbilityData> OnAbilityUsed;
public static event Action<WeaponData> OnWeaponChanged;
public static event Action OnStatsChanged;
public static event Action<string> OnNotificationRequested;
public static void RaiseItemAdded(ItemData item, int amount) => OnItemAdded?.Invoke(item, amount);
public static void RaiseItemRemoved(ItemData item, int amount) => OnItemRemoved?.Invoke(item, amount);
public static void RaiseEnemyKilled(string enemyId) => OnEnemyKilled?.Invoke(enemyId);
public static void RaiseQuestStarted(QuestData quest) => OnQuestStarted?.Invoke(quest);
public static void RaiseQuestProgressChanged(QuestData quest) => OnQuestProgressChanged?.Invoke(quest);
public static void RaiseQuestReadyToComplete(QuestData quest) => OnQuestReadyToComplete?.Invoke(quest);
public static void RaiseQuestCompleted(QuestData quest) => OnQuestCompleted?.Invoke(quest);
public static void RaiseExperienceGained(int amount) => OnExperienceGained?.Invoke(amount);
public static void RaiseLevelUp(int newLevel) => OnLevelUp?.Invoke(newLevel);
public static void RaiseTalentPointChanged(int points) => OnTalentPointChanged?.Invoke(points);
public static void RaiseTalentLearned(TalentData talent) => OnTalentLearned?.Invoke(talent);
public static void RaiseAbilityUsed(AbilityData ability) => OnAbilityUsed?.Invoke(ability);
public static void RaiseWeaponChanged(WeaponData weapon) => OnWeaponChanged?.Invoke(weapon);
public static void RaiseStatsChanged() => OnStatsChanged?.Invoke();
public static void RaiseNotification(string message) => OnNotificationRequested?.Invoke(message);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c92f8d4902c2f63bbb7a3c47c875a94b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+114
View File
@@ -0,0 +1,114 @@
using OTGIntegrated.Abilities;
using OTGIntegrated.Crafting;
using OTGIntegrated.Dialogue;
using OTGIntegrated.Inventory;
using OTGIntegrated.Player;
using OTGIntegrated.Quests;
using OTGIntegrated.Save;
using OTGIntegrated.Stats;
using OTGIntegrated.Talents;
using OTGIntegrated.UI;
using OTGIntegrated.Weapons;
using UnityEngine;
namespace OTGIntegrated.Core
{
public sealed class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[Header("Player")]
public PlayerController playerController;
public PlayerStats playerStats;
public OTGIntegrated.Inventory.Inventory inventory;
public LevelSystem levelSystem;
public WeaponManager weaponManager;
public AbilityManager abilityManager;
public TalentManager talentManager;
[Header("World Systems")]
public QuestManager questManager;
public DialogueManager dialogueManager;
public CraftingSystem craftingSystem;
[Header("UI / Save")]
public UIManager uiManager;
public SaveSystem saveSystem;
private bool initialized;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
ResolveMissingReferences();
InitializeSystems();
}
public void ResolveMissingReferences()
{
if (playerController == null) playerController = FindObjectOfType<PlayerController>();
if (playerStats == null) playerStats = FindObjectOfType<PlayerStats>();
if (inventory == null) inventory = FindObjectOfType<OTGIntegrated.Inventory.Inventory>();
if (levelSystem == null) levelSystem = FindObjectOfType<LevelSystem>();
if (weaponManager == null) weaponManager = FindObjectOfType<WeaponManager>();
if (abilityManager == null) abilityManager = FindObjectOfType<AbilityManager>();
if (talentManager == null) talentManager = FindObjectOfType<TalentManager>();
if (questManager == null) questManager = FindObjectOfType<QuestManager>();
if (dialogueManager == null) dialogueManager = FindObjectOfType<DialogueManager>();
if (craftingSystem == null) craftingSystem = FindObjectOfType<CraftingSystem>();
if (uiManager == null) uiManager = FindObjectOfType<UIManager>();
if (saveSystem == null) saveSystem = FindObjectOfType<SaveSystem>();
}
public void InitializeSystems()
{
if (initialized)
{
return;
}
if (playerController != null) playerController.Initialize(playerStats);
if (levelSystem != null) levelSystem.Initialize(talentManager);
if (weaponManager != null) weaponManager.Initialize(playerStats, inventory);
if (abilityManager != null) abilityManager.Initialize(playerStats);
if (talentManager != null) talentManager.Initialize(playerStats);
if (questManager != null) questManager.Initialize(inventory, levelSystem, talentManager);
if (dialogueManager != null) dialogueManager.Initialize(questManager, uiManager, playerController);
if (craftingSystem != null) craftingSystem.Initialize(inventory, uiManager);
if (uiManager != null) uiManager.Initialize(playerController);
if (saveSystem != null) saveSystem.Initialize(this);
ValidateReferences();
initialized = true;
}
private void ValidateReferences()
{
Require(playerStats, "PlayerStats");
Require(inventory, "Inventory");
Require(levelSystem, "LevelSystem");
Require(weaponManager, "WeaponManager");
Require(abilityManager, "AbilityManager");
Require(talentManager, "TalentManager");
Require(questManager, "QuestManager");
Require(dialogueManager, "DialogueManager");
Require(craftingSystem, "CraftingSystem");
Require(uiManager, "UIManager");
Require(saveSystem, "SaveSystem");
}
private void Require(Object value, string label)
{
if (value == null)
{
Debug.LogWarning($"GameManager reference is missing: {label}", this);
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19f1587ed61214d2181a2d440a8b2922
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4f2542de47c4a49c5b22548156ec5afa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+196
View File
@@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using System.Text;
using OTGIntegrated.Core;
using OTGIntegrated.Player;
using OTGIntegrated.UI;
using UnityEngine;
namespace OTGIntegrated.Crafting
{
public sealed class CraftingSystem : MonoBehaviour, IInteractable
{
public List<RecipeData> availableRecipes = new List<RecipeData>();
[TextArea(1, 3)] public string lastMessage;
private OTGIntegrated.Inventory.Inventory playerInventory;
private UIManager uiManager;
public event Action OnCraftingChanged;
public Transform InteractTransform => transform;
public void Initialize(OTGIntegrated.Inventory.Inventory inventory, UIManager manager)
{
playerInventory = inventory;
uiManager = manager;
Refresh();
}
private void Awake()
{
Collider collider = GetComponent<Collider>();
if (collider != null)
{
collider.isTrigger = true;
}
}
public bool CanCraft(RecipeData recipe)
{
if (recipe == null || playerInventory == null || recipe.resultItem == null)
{
return false;
}
if (recipe.ingredients == null)
{
return false;
}
for (int i = 0; i < recipe.ingredients.Length; i++)
{
Ingredient ingredient = recipe.ingredients[i];
if (ingredient == null || ingredient.item == null || ingredient.amount <= 0)
{
return false;
}
if (!playerInventory.HasItem(ingredient.item, ingredient.amount))
{
return false;
}
}
return playerInventory.CanAddItem(recipe.resultItem, recipe.resultAmount);
}
public bool Craft(RecipeData recipe)
{
if (recipe == null)
{
lastMessage = "Рецепт не выбран.";
Refresh();
return false;
}
if (playerInventory == null)
{
lastMessage = "Инвентарь не подключен.";
Refresh();
return false;
}
if (!HasIngredients(recipe))
{
lastMessage = "Не хватает ресурсов: " + GetMissingIngredientsText(recipe);
GameEvents.RaiseNotification(lastMessage);
Refresh();
return false;
}
if (recipe.resultItem == null || !playerInventory.CanAddItem(recipe.resultItem, recipe.resultAmount))
{
lastMessage = "Недостаточно места для результата.";
GameEvents.RaiseNotification(lastMessage);
Refresh();
return false;
}
for (int i = 0; i < recipe.ingredients.Length; i++)
{
Ingredient ingredient = recipe.ingredients[i];
playerInventory.RemoveItem(ingredient.item, ingredient.amount);
}
playerInventory.AddItem(recipe.resultItem, recipe.resultAmount);
lastMessage = $"Создано: {recipe.resultItem.displayName} x{recipe.resultAmount}";
GameEvents.RaiseNotification(lastMessage);
Refresh();
return true;
}
public string GetMissingIngredientsText(RecipeData recipe)
{
if (recipe == null || recipe.ingredients == null || playerInventory == null)
{
return "нет данных";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < recipe.ingredients.Length; i++)
{
Ingredient ingredient = recipe.ingredients[i];
if (ingredient == null || ingredient.item == null)
{
continue;
}
int current = playerInventory.GetAmount(ingredient.item);
if (current >= ingredient.amount)
{
continue;
}
if (builder.Length > 0)
{
builder.Append(", ");
}
builder.Append(ingredient.item.displayName);
builder.Append(" ");
builder.Append(current);
builder.Append("/");
builder.Append(ingredient.amount);
}
return builder.Length == 0 ? "место в инвентаре" : builder.ToString();
}
public string GetPrompt()
{
return "E — открыть верстак";
}
public bool CanInteract(GameObject interactor)
{
return enabled;
}
public void Interact(GameObject interactor)
{
if (uiManager != null)
{
uiManager.ToggleCrafting();
}
}
public void Refresh()
{
OnCraftingChanged?.Invoke();
}
private bool HasIngredients(RecipeData recipe)
{
if (recipe == null || recipe.ingredients == null || playerInventory == null)
{
return false;
}
for (int i = 0; i < recipe.ingredients.Length; i++)
{
Ingredient ingredient = recipe.ingredients[i];
if (ingredient == null || ingredient.item == null || ingredient.amount <= 0)
{
return false;
}
if (!playerInventory.HasItem(ingredient.item, ingredient.amount))
{
return false;
}
}
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 79a99d064135b6bf6809c0ac449bb5b4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+228
View File
@@ -0,0 +1,228 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace OTGIntegrated.Crafting
{
public sealed class CraftingUI : MonoBehaviour
{
public GameObject panelRoot;
public Transform recipeListRoot;
public Text ingredientsText;
public Text resultText;
public Text descriptionText;
public Text messageText;
public Button craftButton;
public Button closeButton;
public Font uiFont;
private CraftingSystem craftingSystem;
private OTGIntegrated.Inventory.Inventory inventory;
private RecipeData selectedRecipe;
private readonly List<GameObject> spawnedButtons = new List<GameObject>();
public void Initialize(CraftingSystem system, OTGIntegrated.Inventory.Inventory playerInventory)
{
craftingSystem = system;
inventory = playerInventory;
if (craftingSystem != null)
{
craftingSystem.OnCraftingChanged -= Refresh;
craftingSystem.OnCraftingChanged += Refresh;
}
if (inventory != null)
{
inventory.OnInventoryChanged -= Refresh;
inventory.OnInventoryChanged += Refresh;
}
if (craftButton != null)
{
craftButton.onClick.RemoveAllListeners();
craftButton.onClick.AddListener(() =>
{
if (craftingSystem != null && selectedRecipe != null)
{
craftingSystem.Craft(selectedRecipe);
}
});
}
if (closeButton != null)
{
closeButton.onClick.RemoveAllListeners();
closeButton.onClick.AddListener(() => FindObjectOfType<OTGIntegrated.UI.UIManager>()?.CloseCurrentWindow());
}
Refresh();
}
private void OnEnable()
{
Refresh();
}
private void OnDestroy()
{
if (craftingSystem != null)
{
craftingSystem.OnCraftingChanged -= Refresh;
}
if (inventory != null)
{
inventory.OnInventoryChanged -= Refresh;
}
}
public void Refresh()
{
if (recipeListRoot == null || craftingSystem == null)
{
return;
}
ClearButtons();
if (selectedRecipe == null && craftingSystem.availableRecipes.Count > 0)
{
selectedRecipe = craftingSystem.availableRecipes[0];
}
for (int i = 0; i < craftingSystem.availableRecipes.Count; i++)
{
RecipeData recipe = craftingSystem.availableRecipes[i];
CreateRecipeButton(recipe);
}
UpdateDetails();
}
private void CreateRecipeButton(RecipeData recipe)
{
GameObject root = new GameObject(recipe != null ? recipe.displayName : "Recipe", typeof(RectTransform), typeof(Image), typeof(Button));
root.transform.SetParent(recipeListRoot, false);
spawnedButtons.Add(root);
RectTransform rect = root.GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(300f, 46f);
bool canCraft = craftingSystem != null && craftingSystem.CanCraft(recipe);
Image image = root.GetComponent<Image>();
image.color = recipe == selectedRecipe
? new Color(0.82f, 0.58f, 0.18f, 0.95f)
: canCraft ? new Color(0.14f, 0.28f, 0.22f, 0.92f) : new Color(0.12f, 0.13f, 0.16f, 0.88f);
Text label = CreateText(root.transform, "Text", recipe != null ? recipe.displayName : "Recipe", 17, TextAnchor.MiddleLeft);
label.rectTransform.anchorMin = new Vector2(0f, 0f);
label.rectTransform.anchorMax = new Vector2(1f, 1f);
label.rectTransform.offsetMin = new Vector2(14f, 0f);
label.rectTransform.offsetMax = new Vector2(-8f, 0f);
Button button = root.GetComponent<Button>();
button.onClick.AddListener(() =>
{
selectedRecipe = recipe;
Refresh();
});
}
private void UpdateDetails()
{
if (selectedRecipe == null)
{
if (ingredientsText != null) ingredientsText.text = "Рецепт не выбран.";
if (resultText != null) resultText.text = string.Empty;
if (descriptionText != null) descriptionText.text = string.Empty;
if (craftButton != null) craftButton.interactable = false;
return;
}
if (ingredientsText != null)
{
ingredientsText.text = BuildIngredientStatus(selectedRecipe);
}
if (resultText != null)
{
string resultName = selectedRecipe.resultItem != null ? selectedRecipe.resultItem.displayName : "Нет результата";
resultText.text = $"Результат\n{resultName} x{selectedRecipe.resultAmount}";
}
if (descriptionText != null)
{
descriptionText.text = selectedRecipe.description;
}
if (messageText != null)
{
messageText.text = craftingSystem != null ? craftingSystem.lastMessage : string.Empty;
}
if (craftButton != null)
{
craftButton.interactable = craftingSystem != null && craftingSystem.CanCraft(selectedRecipe);
}
}
private string BuildIngredientStatus(RecipeData recipe)
{
if (recipe == null || recipe.ingredients == null)
{
return "Ингредиенты отсутствуют.";
}
System.Text.StringBuilder builder = new System.Text.StringBuilder();
builder.AppendLine("Ингредиенты");
for (int i = 0; i < recipe.ingredients.Length; i++)
{
Ingredient ingredient = recipe.ingredients[i];
if (ingredient == null || ingredient.item == null)
{
continue;
}
int owned = inventory != null ? inventory.GetAmount(ingredient.item) : 0;
bool enough = owned >= ingredient.amount;
builder.Append(enough ? "<color=#88E08A>" : "<color=#FF8888>");
builder.Append(ingredient.item.displayName);
builder.Append(": ");
builder.Append(owned);
builder.Append(" / ");
builder.Append(ingredient.amount);
builder.AppendLine("</color>");
}
return builder.ToString();
}
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.text = value;
text.font = uiFont != null ? uiFont : Resources.GetBuiltinResource<Font>("Arial.ttf");
text.fontSize = fontSize;
text.alignment = anchor;
text.supportRichText = true;
text.color = new Color(0.94f, 0.93f, 0.88f, 1f);
return text;
}
private void ClearButtons()
{
for (int i = 0; i < spawnedButtons.Count; i++)
{
if (spawnedButtons[i] != null)
{
Destroy(spawnedButtons[i]);
}
}
spawnedButtons.Clear();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a9f9af7e28388a641b3ae88edb6b98e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+69
View File
@@ -0,0 +1,69 @@
using System;
using System.Text;
using OTGIntegrated.Items;
using UnityEngine;
namespace OTGIntegrated.Crafting
{
[Serializable]
public sealed class Ingredient
{
public ItemData item;
[Min(1)] public int amount = 1;
}
[CreateAssetMenu(fileName = "RecipeData", menuName = "OTG Integrated/Recipe Data")]
public sealed class RecipeData : ScriptableObject
{
public string recipeId;
public string displayName;
[TextArea(2, 5)] public string description;
public Ingredient[] ingredients;
public ItemData resultItem;
[Min(1)] public int resultAmount = 1;
private void OnValidate()
{
if (string.IsNullOrWhiteSpace(recipeId))
{
recipeId = name;
}
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = recipeId;
}
resultAmount = Mathf.Max(1, resultAmount);
}
public string GetIngredientsText()
{
if (ingredients == null || ingredients.Length == 0)
{
return "Без ингредиентов";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < ingredients.Length; i++)
{
Ingredient ingredient = ingredients[i];
if (ingredient == null || ingredient.item == null)
{
continue;
}
if (builder.Length > 0)
{
builder.Append(", ");
}
builder.Append(ingredient.item.displayName);
builder.Append(" x");
builder.Append(ingredient.amount);
}
return builder.Length == 0 ? "Без ингредиентов" : builder.ToString();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9b0fcbd4f1eda0f44a96202d7011d01e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7ac0e9b4de9dd725f85d3f39f1b6ffa1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+249
View File
@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using OTGIntegrated.Core;
using OTGIntegrated.Player;
using OTGIntegrated.Quests;
using OTGIntegrated.UI;
using UnityEngine;
namespace OTGIntegrated.Dialogue
{
public sealed class DialogueManager : MonoBehaviour
{
public DialogueUI dialogueUI;
private QuestManager questManager;
private UIManager uiManager;
private PlayerController playerController;
private NPCInteract currentNpc;
private DialogueNode currentNode;
public bool IsDialogueActive { get; private set; }
public void Initialize(QuestManager questManager, UIManager uiManager, PlayerController playerController)
{
this.questManager = questManager;
this.uiManager = uiManager;
this.playerController = playerController;
if (dialogueUI == null)
{
dialogueUI = FindObjectOfType<DialogueUI>(true);
}
}
private void Update()
{
if (IsDialogueActive && Input.GetKeyDown(KeyCode.Escape))
{
EndDialogue();
}
}
public void StartDialogue(NPCInteract npc)
{
if (npc == null || npc.startNode == null)
{
return;
}
currentNpc = npc;
IsDialogueActive = true;
questManager?.NotifyNpcTalked(npc.npcId);
if (uiManager != null)
{
uiManager.OpenDialogue();
}
else if (playerController != null)
{
playerController.SetInputEnabled(false);
}
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
ShowNode(npc.startNode);
}
public void ShowNode(DialogueNode node)
{
if (node == null)
{
EndDialogue();
return;
}
currentNode = node;
string speaker = !string.IsNullOrWhiteSpace(node.speakerName)
? node.speakerName
: currentNpc != null ? currentNpc.displayName : "NPC";
List<DialogueChoice> choices = BuildChoices(node);
dialogueUI?.Show(speaker, node.npcLine, choices);
}
public void EndDialogue()
{
if (!IsDialogueActive)
{
return;
}
IsDialogueActive = false;
currentNpc = null;
currentNode = null;
dialogueUI?.Hide();
if (uiManager != null)
{
uiManager.CloseDialogue();
}
else if (playerController != null)
{
playerController.SetInputEnabled(true);
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
private List<DialogueChoice> BuildChoices(DialogueNode node)
{
List<DialogueChoice> choices = new List<DialogueChoice>();
if (currentNpc != null && currentNpc.offeredQuest != null && questManager != null)
{
QuestData quest = currentNpc.offeredQuest;
if (!questManager.IsQuestActive(quest) && !questManager.IsQuestCompleted(quest))
{
choices.Add(new DialogueChoice($"Взять квест: {quest.displayName}", () =>
{
questManager.StartQuest(quest);
EndDialogue();
}));
}
}
if (currentNpc != null && currentNpc.completionQuest != null && questManager != null)
{
QuestData quest = currentNpc.completionQuest;
if (questManager.IsQuestReady(quest))
{
choices.Add(new DialogueChoice($"Завершить квест: {quest.displayName}", () =>
{
questManager.TryCompleteQuest(quest, currentNpc.npcId);
EndDialogue();
}));
}
}
if (node.responses != null)
{
for (int i = 0; i < node.responses.Length; i++)
{
DialogueResponse response = node.responses[i];
if (response == null || string.IsNullOrWhiteSpace(response.responseText))
{
continue;
}
choices.Add(new DialogueChoice(response.responseText, () => SelectResponse(response)));
}
}
if (currentNpc != null && currentNpc.canOpenCrafting)
{
choices.Add(new DialogueChoice("Открыть крафт", () =>
{
EndDialogue();
uiManager?.OpenCrafting();
}));
}
choices.Add(new DialogueChoice("Закончить разговор", EndDialogue));
return choices;
}
private void SelectResponse(DialogueResponse response)
{
if (response == null)
{
EndDialogue();
return;
}
if (response.questToStart != null && questManager != null)
{
questManager.StartQuest(response.questToStart);
}
if (response.questToComplete != null && currentNpc != null && questManager != null)
{
questManager.TryCompleteQuest(response.questToComplete, currentNpc.npcId);
}
if (response.openCrafting)
{
EndDialogue();
uiManager?.OpenCrafting();
return;
}
if (!response.endsDialogue && response.nextNode == null)
{
ShowInformationalFollowUp();
return;
}
if (response.endsDialogue || response.nextNode == null)
{
EndDialogue();
}
else
{
ShowNode(response.nextNode);
}
}
private void ShowInformationalFollowUp()
{
string speaker = currentNpc != null ? currentNpc.displayName : currentNode != null ? currentNode.speakerName : "NPC";
string text = GetInformationalText();
List<DialogueChoice> choices = new List<DialogueChoice>
{
new DialogueChoice("Назад", () => ShowNode(currentNode)),
new DialogueChoice("Закончить разговор", EndDialogue)
};
dialogueUI?.Show(speaker, text, choices);
}
private string GetInformationalText()
{
string npcId = currentNpc != null ? currentNpc.npcId : string.Empty;
switch (npcId)
{
case "Elder":
return "Нужна обычная древесина из леса. Собери 5 Wood, затем вернись ко мне, чтобы завершить квест.";
case "Guard":
return "Гоблины стоят в лагере на юго-востоке. Уничтожь 3 Goblin оружием или способностями и возвращайся за наградой.";
case "Blacksmith":
return "Для Iron Axe нужны Wood x2, Stone x2 и Iron Ore x1. Открой верстак через E рядом со столом или клавишей C.";
case "Mage":
return "Fireball на 1 тратит ману и наносит урон. Heal на 2 лечит. Blink на 3 переносит вперёд. Таланты открываются на K.";
default:
return currentNode != null ? currentNode.npcLine : "Подробностей пока нет.";
}
}
}
public sealed class DialogueChoice
{
public string Text { get; }
public Action Callback { get; }
public DialogueChoice(string text, Action callback)
{
Text = text;
Callback = callback;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d7afb1a8b0b1bd1ca745e01c7173a1f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+35
View File
@@ -0,0 +1,35 @@
using System;
using OTGIntegrated.Quests;
using UnityEngine;
namespace OTGIntegrated.Dialogue
{
[CreateAssetMenu(fileName = "DialogueNode", menuName = "OTG Integrated/Dialogue Node")]
public sealed class DialogueNode : ScriptableObject
{
public string nodeId;
public string speakerName;
[TextArea(3, 8)] public string npcLine;
public DialogueResponse[] responses;
public bool isEndNode;
private void OnValidate()
{
if (string.IsNullOrWhiteSpace(nodeId))
{
nodeId = name;
}
}
}
[Serializable]
public sealed class DialogueResponse
{
public string responseText;
public DialogueNode nextNode;
public bool endsDialogue;
public QuestData questToStart;
public QuestData questToComplete;
public bool openCrafting;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4a1de7da3c08add33ba7a3709b3c8fde
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+141
View File
@@ -0,0 +1,141 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace OTGIntegrated.Dialogue
{
public sealed class DialogueUI : MonoBehaviour
{
public GameObject panelRoot;
public Text speakerText;
public Text lineText;
public Transform responseRoot;
public Button closeButton;
public Font uiFont;
private readonly List<GameObject> spawnedResponses = new List<GameObject>();
private void Awake()
{
ConfigureResponseLayout();
if (closeButton != null)
{
closeButton.onClick.RemoveAllListeners();
closeButton.onClick.AddListener(() => FindObjectOfType<DialogueManager>()?.EndDialogue());
}
Hide();
}
public void Show(string speaker, string line, IReadOnlyList<DialogueChoice> choices)
{
if (panelRoot != null)
{
panelRoot.SetActive(true);
}
if (speakerText != null)
{
speakerText.text = speaker;
}
if (lineText != null)
{
lineText.text = line;
}
ClearResponses();
if (choices == null || responseRoot == null)
{
return;
}
for (int i = 0; i < choices.Count; i++)
{
CreateChoiceButton(choices[i]);
}
}
public void Hide()
{
ClearResponses();
if (panelRoot != null)
{
panelRoot.SetActive(false);
}
}
private void CreateChoiceButton(DialogueChoice choice)
{
if (choice == null)
{
return;
}
GameObject root = new GameObject("DialogueChoice", typeof(RectTransform), typeof(Image), typeof(Button));
root.transform.SetParent(responseRoot, false);
spawnedResponses.Add(root);
root.GetComponent<RectTransform>().sizeDelta = new Vector2(0f, 42f);
LayoutElement layoutElement = root.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 42f;
layoutElement.minHeight = 42f;
layoutElement.flexibleWidth = 1f;
root.GetComponent<Image>().color = new Color(0.11f, 0.14f, 0.18f, 0.94f);
Text label = CreateText(root.transform, "Text", choice.Text, 16, TextAnchor.MiddleLeft);
label.rectTransform.anchorMin = Vector2.zero;
label.rectTransform.anchorMax = Vector2.one;
label.rectTransform.offsetMin = new Vector2(14f, 0f);
label.rectTransform.offsetMax = new Vector2(-8f, 0f);
Button button = root.GetComponent<Button>();
button.onClick.AddListener(() => choice.Callback?.Invoke());
}
private void ConfigureResponseLayout()
{
if (responseRoot == null)
{
return;
}
VerticalLayoutGroup layout = responseRoot.GetComponent<VerticalLayoutGroup>();
if (layout == null)
{
return;
}
layout.childControlWidth = true;
layout.childForceExpandWidth = true;
layout.childControlHeight = false;
layout.childForceExpandHeight = false;
}
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 ClearResponses()
{
for (int i = 0; i < spawnedResponses.Count; i++)
{
if (spawnedResponses[i] != null)
{
Destroy(spawnedResponses[i]);
}
}
spawnedResponses.Clear();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35cd056b2ce856f5fb88e5b86a77090a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+50
View File
@@ -0,0 +1,50 @@
using OTGIntegrated.Player;
using OTGIntegrated.Quests;
using UnityEngine;
namespace OTGIntegrated.Dialogue
{
[RequireComponent(typeof(Collider))]
public sealed class NPCInteract : MonoBehaviour, IInteractable
{
public string npcId;
public string displayName = "NPC";
public DialogueNode startNode;
public QuestData offeredQuest;
public QuestData completionQuest;
public bool canOpenCrafting;
private DialogueManager dialogueManager;
public Transform InteractTransform => transform;
private void Awake()
{
Collider ownCollider = GetComponent<Collider>();
ownCollider.isTrigger = true;
}
private void Start()
{
dialogueManager = FindObjectOfType<DialogueManager>();
}
public string GetPrompt()
{
return $"E — поговорить: {displayName}";
}
public bool CanInteract(GameObject interactor)
{
return startNode != null && dialogueManager != null;
}
public void Interact(GameObject interactor)
{
if (dialogueManager != null)
{
dialogueManager.StartDialogue(this);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a592109dfa0273ec9b701c9ff20e1fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 23725abb957f3f8c089b027d1a056689
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+213
View File
@@ -0,0 +1,213 @@
using System;
using System.Collections.Generic;
using OTGIntegrated.Core;
using OTGIntegrated.Items;
using UnityEngine;
namespace OTGIntegrated.Inventory
{
public sealed class Inventory : MonoBehaviour
{
[SerializeField] private int maxSlots = 20;
[SerializeField] private List<InventorySlot> slots = new List<InventorySlot>();
public int MaxSlots => maxSlots;
public event Action OnInventoryChanged;
public bool AddItem(ItemData item, int amount)
{
if (!CanAddItem(item, amount))
{
GameEvents.RaiseNotification("Недостаточно места в инвентаре");
return false;
}
AddItemInternal(item, amount);
RaiseChanged();
GameEvents.RaiseItemAdded(item, amount);
GameEvents.RaiseNotification($"+{amount} {item.displayName}");
return true;
}
public bool RemoveItem(ItemData item, int amount)
{
if (item == null || amount <= 0 || !HasItem(item, amount))
{
return false;
}
int remaining = amount;
for (int i = slots.Count - 1; i >= 0 && remaining > 0; i--)
{
InventorySlot slot = slots[i];
if (slot == null || slot.item != item || slot.amount <= 0)
{
continue;
}
int removed = Mathf.Min(slot.amount, remaining);
slot.amount -= removed;
remaining -= removed;
if (slot.IsEmpty())
{
slots.RemoveAt(i);
}
}
RaiseChanged();
GameEvents.RaiseItemRemoved(item, amount);
return true;
}
public bool HasItem(ItemData item, int amount)
{
return item != null && amount > 0 && GetAmount(item) >= amount;
}
public int GetAmount(ItemData item)
{
if (item == null)
{
return 0;
}
int total = 0;
for (int i = 0; i < slots.Count; i++)
{
InventorySlot slot = slots[i];
if (slot != null && slot.item == item && slot.amount > 0)
{
total += slot.amount;
}
}
return total;
}
public IReadOnlyList<InventorySlot> GetAllSlots()
{
return slots;
}
public void Clear()
{
slots.Clear();
RaiseChanged();
}
public bool CanAddItem(ItemData item, int amount)
{
if (item == null || amount <= 0)
{
return false;
}
int remaining = amount;
int stackSize = Mathf.Max(1, item.maxStack);
for (int i = 0; i < slots.Count; i++)
{
InventorySlot slot = slots[i];
if (slot != null && slot.item == item && slot.amount > 0 && slot.amount < stackSize)
{
remaining -= stackSize - slot.amount;
if (remaining <= 0)
{
return true;
}
}
}
int freeSlots = Mathf.Max(0, maxSlots - slots.Count);
int requiredSlots = Mathf.CeilToInt(remaining / (float)stackSize);
return requiredSlots <= freeSlots;
}
public List<InventorySlotSaveData> SaveData()
{
List<InventorySlotSaveData> saveSlots = new List<InventorySlotSaveData>();
for (int i = 0; i < slots.Count; i++)
{
InventorySlot slot = slots[i];
if (slot == null || slot.IsEmpty())
{
continue;
}
saveSlots.Add(new InventorySlotSaveData
{
itemId = slot.item.itemId,
amount = slot.amount
});
}
return saveSlots;
}
public void LoadData(List<InventorySlotSaveData> saveSlots, IEnumerable<ItemData> itemCatalog)
{
slots.Clear();
Dictionary<string, ItemData> lookup = new Dictionary<string, ItemData>(StringComparer.Ordinal);
if (itemCatalog != null)
{
foreach (ItemData item in itemCatalog)
{
if (item != null && !string.IsNullOrWhiteSpace(item.itemId))
{
lookup[item.itemId] = item;
}
}
}
if (saveSlots != null)
{
for (int i = 0; i < saveSlots.Count; i++)
{
InventorySlotSaveData slot = saveSlots[i];
if (slot == null || string.IsNullOrWhiteSpace(slot.itemId) || slot.amount <= 0)
{
continue;
}
if (lookup.TryGetValue(slot.itemId, out ItemData item))
{
AddItemInternal(item, slot.amount);
}
}
}
RaiseChanged();
}
private void AddItemInternal(ItemData item, int amount)
{
int remaining = amount;
int stackSize = Mathf.Max(1, item.maxStack);
for (int i = 0; i < slots.Count && remaining > 0; i++)
{
InventorySlot slot = slots[i];
if (slot == null || slot.item != item || slot.amount >= stackSize)
{
continue;
}
int added = Mathf.Min(stackSize - slot.amount, remaining);
slot.amount += added;
remaining -= added;
}
while (remaining > 0 && slots.Count < maxSlots)
{
int added = Mathf.Min(stackSize, remaining);
slots.Add(new InventorySlot(item, added));
remaining -= added;
}
}
private void RaiseChanged()
{
OnInventoryChanged?.Invoke();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 828686a030f167d54974e4f844de7052
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,35 @@
using System;
using OTGIntegrated.Items;
namespace OTGIntegrated.Inventory
{
[Serializable]
public sealed class InventorySlot
{
public ItemData item;
public int amount;
public InventorySlot(ItemData item, int amount)
{
this.item = item;
this.amount = amount;
}
public bool IsEmpty()
{
return item == null || amount <= 0;
}
public string GetDisplayText()
{
return IsEmpty() ? string.Empty : $"{item.displayName} x{amount}";
}
}
[Serializable]
public sealed class InventorySlotSaveData
{
public string itemId;
public int amount;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4e8e76c6c6c64018fbfed8c77e04af66
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+161
View File
@@ -0,0 +1,161 @@
using System.Collections.Generic;
using OTGIntegrated.Items;
using UnityEngine;
using UnityEngine.UI;
namespace OTGIntegrated.Inventory
{
public sealed class InventoryUI : MonoBehaviour
{
public GameObject panelRoot;
public Transform slotGrid;
public Text titleText;
public Text descriptionText;
public Text hintText;
public Button closeButton;
public Font uiFont;
private Inventory inventory;
private readonly List<GameObject> spawnedSlots = new List<GameObject>();
public void Initialize(Inventory inventory)
{
this.inventory = inventory;
if (inventory != null)
{
inventory.OnInventoryChanged -= Refresh;
inventory.OnInventoryChanged += Refresh;
}
if (closeButton != null)
{
closeButton.onClick.RemoveAllListeners();
closeButton.onClick.AddListener(() => FindObjectOfType<OTGIntegrated.UI.UIManager>()?.CloseCurrentWindow());
}
Refresh();
}
private void OnEnable()
{
Refresh();
}
private void OnDestroy()
{
if (inventory != null)
{
inventory.OnInventoryChanged -= Refresh;
}
}
public void Refresh()
{
if (slotGrid == null)
{
return;
}
ClearSlots();
IReadOnlyList<InventorySlot> slots = inventory != null ? inventory.GetAllSlots() : null;
int maxSlots = inventory != null ? inventory.MaxSlots : 20;
for (int i = 0; i < maxSlots; i++)
{
InventorySlot slot = slots != null && i < slots.Count ? slots[i] : null;
CreateSlot(i, slot);
}
if (titleText != null)
{
titleText.text = "Инвентарь";
}
if (hintText != null)
{
hintText.text = "I — закрыть F5 — сохранить F9 — загрузить";
}
if (descriptionText != null && string.IsNullOrEmpty(descriptionText.text))
{
descriptionText.text = "Выберите предмет.";
}
}
private void CreateSlot(int index, InventorySlot slot)
{
GameObject root = new GameObject($"InventorySlot_{index}", typeof(RectTransform), typeof(Image), typeof(Button));
root.transform.SetParent(slotGrid, false);
spawnedSlots.Add(root);
RectTransform rect = root.GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(90f, 90f);
Image background = root.GetComponent<Image>();
background.color = slot != null && !slot.IsEmpty()
? Color.Lerp(new Color(0.09f, 0.1f, 0.12f, 0.95f), slot.item.itemColor, 0.35f)
: new Color(0.07f, 0.08f, 0.1f, 0.82f);
Text label = CreateText(root.transform, "Label", slot != null && !slot.IsEmpty() ? slot.item.displayName : string.Empty, 14, TextAnchor.MiddleCenter);
label.rectTransform.anchorMin = new Vector2(0.08f, 0.24f);
label.rectTransform.anchorMax = new Vector2(0.92f, 0.78f);
label.rectTransform.offsetMin = Vector2.zero;
label.rectTransform.offsetMax = Vector2.zero;
Text count = CreateText(root.transform, "Count", slot != null && !slot.IsEmpty() ? slot.amount.ToString() : string.Empty, 14, TextAnchor.LowerRight);
count.rectTransform.anchorMin = new Vector2(0.1f, 0.05f);
count.rectTransform.anchorMax = new Vector2(0.95f, 0.35f);
count.rectTransform.offsetMin = Vector2.zero;
count.rectTransform.offsetMax = Vector2.zero;
Button button = root.GetComponent<Button>();
button.onClick.AddListener(() => SelectSlot(slot));
}
private void SelectSlot(InventorySlot slot)
{
if (descriptionText == null)
{
return;
}
if (slot == null || slot.IsEmpty())
{
descriptionText.text = "Пустой слот.";
return;
}
ItemData item = slot.item;
descriptionText.text = $"{item.displayName}\n\n{item.description}\n\nТип: {item.itemType}\nКоличество: {slot.amount}";
}
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.text = value;
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.resizeTextForBestFit = true;
text.resizeTextMinSize = 10;
text.resizeTextMaxSize = fontSize;
return text;
}
private void ClearSlots()
{
for (int i = 0; i < spawnedSlots.Count; i++)
{
if (spawnedSlots[i] != null)
{
Destroy(spawnedSlots[i]);
}
}
spawnedSlots.Clear();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 414a98535c9cc47948e3da48ecba4e03
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2c0053840eae6c9b288b0745993dfd14
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+40
View File
@@ -0,0 +1,40 @@
using UnityEngine;
namespace OTGIntegrated.Items
{
public enum ItemType
{
Resource,
Consumable,
Weapon,
Quest,
Crafted
}
[CreateAssetMenu(fileName = "ItemData", menuName = "OTG Integrated/Item Data")]
public sealed class ItemData : ScriptableObject
{
public string itemId;
public string displayName;
[TextArea(2, 5)] public string description;
[Min(1)] public int maxStack = 20;
public ItemType itemType = ItemType.Resource;
public Color itemColor = Color.white;
public Sprite icon;
private void OnValidate()
{
if (string.IsNullOrWhiteSpace(itemId))
{
itemId = name;
}
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = itemId;
}
maxStack = Mathf.Max(1, maxStack);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 078fc01ab57931f239a827eff5dce814
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+44
View File
@@ -0,0 +1,44 @@
using OTGIntegrated.Core;
using OTGIntegrated.Inventory;
using OTGIntegrated.Player;
using UnityEngine;
namespace OTGIntegrated.Items
{
[RequireComponent(typeof(Collider))]
public sealed class ItemPickup : MonoBehaviour, IInteractable
{
public ItemData itemData;
[Min(1)] public int amount = 1;
public Transform InteractTransform => transform;
private void Awake()
{
Collider ownCollider = GetComponent<Collider>();
ownCollider.isTrigger = true;
}
public string GetPrompt()
{
string label = itemData != null ? itemData.displayName : "Предмет";
return $"E — подобрать: {label}";
}
public bool CanInteract(GameObject interactor)
{
return itemData != null && amount > 0;
}
public void Interact(GameObject interactor)
{
OTGIntegrated.Inventory.Inventory inventory = GameManager.Instance != null
? GameManager.Instance.inventory
: interactor.GetComponentInChildren<OTGIntegrated.Inventory.Inventory>();
if (inventory != null && inventory.AddItem(itemData, amount))
{
Destroy(gameObject);
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 662778782848d943092b5de50a866096
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+90
View File
@@ -0,0 +1,90 @@
using System.Collections;
using OTGIntegrated.Core;
using OTGIntegrated.Player;
using UnityEngine;
namespace OTGIntegrated.Items
{
[RequireComponent(typeof(Collider))]
public sealed class ResourceNode : MonoBehaviour, IInteractable
{
public ItemData itemData;
[Min(1)] public int amount = 1;
[Min(0f)] public float respawnCooldown = 0f;
public string actionLabel = "собрать";
private Collider nodeCollider;
private Renderer[] renderers;
private bool depleted;
public Transform InteractTransform => transform;
private void Awake()
{
nodeCollider = GetComponent<Collider>();
nodeCollider.isTrigger = true;
renderers = GetComponentsInChildren<Renderer>();
}
public string GetPrompt()
{
string label = itemData != null ? itemData.displayName : "Ресурс";
return $"E — {actionLabel}: {label}";
}
public bool CanInteract(GameObject interactor)
{
return !depleted && itemData != null && amount > 0;
}
public void Interact(GameObject interactor)
{
if (!CanInteract(interactor))
{
return;
}
OTGIntegrated.Inventory.Inventory inventory = GameManager.Instance != null
? GameManager.Instance.inventory
: interactor.GetComponentInChildren<OTGIntegrated.Inventory.Inventory>();
if (inventory == null || !inventory.AddItem(itemData, amount))
{
return;
}
if (respawnCooldown > 0f)
{
StartCoroutine(RespawnRoutine());
}
else
{
Destroy(gameObject);
}
}
private IEnumerator RespawnRoutine()
{
SetDepleted(true);
yield return new WaitForSeconds(respawnCooldown);
SetDepleted(false);
}
private void SetDepleted(bool value)
{
depleted = value;
if (nodeCollider != null)
{
nodeCollider.enabled = !value;
}
for (int i = 0; i < renderers.Length; i++)
{
if (renderers[i] != null)
{
renderers[i].enabled = !value;
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4a33ea5d7040fcd0c92797efb964064a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0a277f00581ad6576a0d5b045321746e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+139
View File
@@ -0,0 +1,139 @@
using OTGIntegrated.Stats;
using UnityEngine;
namespace OTGIntegrated.Player
{
[RequireComponent(typeof(CharacterController))]
public sealed class PlayerController : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float rotationSpeed = 12f;
[SerializeField] private float gravity = -24f;
[Header("Camera")]
public Transform cameraRoot;
public Camera playerCamera;
[SerializeField] private Vector3 cameraOffset = new Vector3(0f, 8.5f, -12.5f);
[SerializeField] private float mouseSensitivity = 2.2f;
private CharacterController characterController;
private PlayerStats playerStats;
private Vector3 velocity;
private float cameraYaw;
private bool inputEnabled = true;
public bool InputEnabled => inputEnabled;
public void Initialize(PlayerStats stats)
{
playerStats = stats != null ? stats : GetComponent<PlayerStats>();
}
private void Awake()
{
characterController = GetComponent<CharacterController>();
if (playerStats == null)
{
playerStats = GetComponent<PlayerStats>();
}
if (playerCamera == null)
{
playerCamera = Camera.main;
}
if (cameraRoot == null)
{
GameObject root = new GameObject("CameraRoot");
cameraRoot = root.transform;
cameraRoot.position = transform.position;
}
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
private void Update()
{
if (!inputEnabled)
{
return;
}
HandleCameraInput();
HandleMovement();
}
private void LateUpdate()
{
UpdateCamera();
}
public void SetInputEnabled(bool enabled)
{
inputEnabled = enabled;
if (!enabled)
{
velocity.x = 0f;
velocity.z = 0f;
}
}
public Vector3 GetAimForward()
{
Vector3 forward = playerCamera != null ? playerCamera.transform.forward : transform.forward;
forward.y = 0f;
return forward.sqrMagnitude > 0.001f ? forward.normalized : transform.forward;
}
private void HandleCameraInput()
{
cameraYaw += Input.GetAxis("Mouse X") * mouseSensitivity;
}
private void HandleMovement()
{
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
Vector3 input = new Vector3(horizontal, 0f, vertical);
input = Vector3.ClampMagnitude(input, 1f);
Quaternion yawRotation = Quaternion.Euler(0f, cameraYaw, 0f);
Vector3 move = yawRotation * input;
float moveSpeed = playerStats != null ? playerStats.MoveSpeed : 5f;
if (move.sqrMagnitude > 0.001f)
{
Quaternion targetRotation = Quaternion.LookRotation(move, Vector3.up);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
}
if (characterController.isGrounded && velocity.y < 0f)
{
velocity.y = -2f;
}
velocity.y += gravity * Time.deltaTime;
Vector3 motion = move * moveSpeed + Vector3.up * velocity.y;
characterController.Move(motion * Time.deltaTime);
}
private void UpdateCamera()
{
if (cameraRoot == null)
{
return;
}
cameraRoot.position = transform.position;
cameraRoot.rotation = Quaternion.Euler(0f, cameraYaw, 0f);
if (playerCamera != null)
{
Vector3 offset = Quaternion.Euler(0f, cameraYaw, 0f) * cameraOffset;
playerCamera.transform.position = transform.position + offset;
playerCamera.transform.LookAt(transform.position + Vector3.up * 1.1f);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2e1e9d509df7e54aa12d3ba20855357
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+105
View File
@@ -0,0 +1,105 @@
using OTGIntegrated.UI;
using UnityEngine;
namespace OTGIntegrated.Player
{
public interface IInteractable
{
Transform InteractTransform { get; }
string GetPrompt();
bool CanInteract(GameObject interactor);
void Interact(GameObject interactor);
}
public sealed class PlayerInteraction : MonoBehaviour
{
[SerializeField] private float interactionRadius = 2.6f;
[SerializeField] private LayerMask interactionMask = ~0;
public InteractionPromptUI promptUI;
private readonly Collider[] hits = new Collider[24];
private PlayerController playerController;
private IInteractable currentInteractable;
private void Awake()
{
playerController = GetComponent<PlayerController>();
if (promptUI == null)
{
promptUI = FindObjectOfType<InteractionPromptUI>();
}
}
private void Update()
{
if (playerController != null && !playerController.InputEnabled)
{
SetCurrent(null);
return;
}
FindNearestInteractable();
if (currentInteractable != null && Input.GetKeyDown(KeyCode.E))
{
currentInteractable.Interact(gameObject);
}
}
private void FindNearestInteractable()
{
int count = Physics.OverlapSphereNonAlloc(transform.position, interactionRadius, hits, interactionMask, QueryTriggerInteraction.Collide);
IInteractable nearest = null;
float nearestDistance = float.MaxValue;
for (int i = 0; i < count; i++)
{
Collider hit = hits[i];
if (hit == null)
{
continue;
}
IInteractable interactable = hit.GetComponentInParent<IInteractable>();
if (interactable == null || !interactable.CanInteract(gameObject))
{
continue;
}
Transform interactTransform = interactable.InteractTransform != null ? interactable.InteractTransform : hit.transform;
float distance = (interactTransform.position - transform.position).sqrMagnitude;
if (distance < nearestDistance)
{
nearestDistance = distance;
nearest = interactable;
}
}
SetCurrent(nearest);
}
private void SetCurrent(IInteractable interactable)
{
currentInteractable = interactable;
if (promptUI == null)
{
return;
}
if (currentInteractable == null)
{
promptUI.Hide();
}
else
{
promptUI.Show(currentInteractable.GetPrompt());
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = new Color(1f, 0.75f, 0.2f, 0.25f);
Gizmos.DrawSphere(transform.position, interactionRadius);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: def57f8de123682a59566f0e184ccdf3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0acf6bd5ac3233ec49f28bd9ee809944
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+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:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7af1ab916f75fd9de86085a622817517
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+24
View File
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using OTGIntegrated.Inventory;
using OTGIntegrated.Quests;
using UnityEngine;
namespace OTGIntegrated.Save
{
[Serializable]
public sealed class GameSaveData
{
public Vector3 playerPosition;
public float currentHealth;
public float currentMana;
public int level;
public int currentExp;
public int expToNextLevel;
public int talentPoints;
public List<string> learnedTalentIds = new List<string>();
public List<QuestSaveEntry> quests = new List<QuestSaveEntry>();
public List<InventorySlotSaveData> inventory = new List<InventorySlotSaveData>();
public string currentWeaponId;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88679d8b286ea9b998dee5138457108f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+152
View File
@@ -0,0 +1,152 @@
using System.Collections.Generic;
using OTGIntegrated.Core;
using OTGIntegrated.Items;
using UnityEngine;
namespace OTGIntegrated.Save
{
public sealed class SaveSystem : MonoBehaviour
{
private const string SaveKey = "OTG_Lab08_IntegratedRPG_Save";
public List<ItemData> itemCatalog = new List<ItemData>();
private GameManager gameManager;
public void Initialize(GameManager manager)
{
gameManager = manager;
}
public void Save()
{
if (gameManager == null)
{
gameManager = GameManager.Instance;
}
if (gameManager == null)
{
return;
}
GameSaveData data = new GameSaveData();
if (gameManager.playerController != null)
{
data.playerPosition = gameManager.playerController.transform.position;
}
if (gameManager.playerStats != null)
{
data.currentHealth = gameManager.playerStats.CurrentHealth;
data.currentMana = gameManager.playerStats.CurrentMana;
}
if (gameManager.levelSystem != null)
{
data.level = gameManager.levelSystem.level;
data.currentExp = gameManager.levelSystem.currentExp;
data.expToNextLevel = gameManager.levelSystem.expToNextLevel;
}
if (gameManager.talentManager != null)
{
data.talentPoints = gameManager.talentManager.availableTalentPoints;
data.learnedTalentIds = new List<string>(gameManager.talentManager.LearnedTalentIds);
}
if (gameManager.questManager != null)
{
data.quests = gameManager.questManager.SaveData();
}
if (gameManager.inventory != null)
{
data.inventory = gameManager.inventory.SaveData();
}
if (gameManager.weaponManager != null)
{
data.currentWeaponId = gameManager.weaponManager.SaveData();
}
PlayerPrefs.SetString(SaveKey, JsonUtility.ToJson(data));
PlayerPrefs.Save();
GameEvents.RaiseNotification("Прогресс сохранён");
}
public void Load()
{
if (!PlayerPrefs.HasKey(SaveKey))
{
GameEvents.RaiseNotification("Сохранение не найдено");
return;
}
if (gameManager == null)
{
gameManager = GameManager.Instance;
}
if (gameManager == null)
{
return;
}
GameSaveData data = JsonUtility.FromJson<GameSaveData>(PlayerPrefs.GetString(SaveKey));
if (data == null)
{
GameEvents.RaiseNotification("Сохранение повреждено");
return;
}
if (gameManager.playerController != null)
{
CharacterController controller = gameManager.playerController.GetComponent<CharacterController>();
if (controller != null) controller.enabled = false;
gameManager.playerController.transform.position = data.playerPosition;
if (controller != null) controller.enabled = true;
}
if (gameManager.levelSystem != null)
{
gameManager.levelSystem.SetProgress(data.level, data.currentExp, data.expToNextLevel);
}
if (gameManager.inventory != null)
{
gameManager.inventory.LoadData(data.inventory, itemCatalog);
}
if (gameManager.talentManager != null)
{
gameManager.talentManager.LoadProgress(data.talentPoints, data.learnedTalentIds);
}
if (gameManager.questManager != null)
{
gameManager.questManager.LoadData(data.quests);
}
if (gameManager.weaponManager != null)
{
gameManager.weaponManager.LoadData(data.currentWeaponId);
}
if (gameManager.playerStats != null)
{
gameManager.playerStats.SetRuntimeVitals(data.currentHealth, data.currentMana);
}
GameEvents.RaiseNotification("Прогресс загружен");
}
public void ResetSave()
{
PlayerPrefs.DeleteKey(SaveKey);
PlayerPrefs.Save();
GameEvents.RaiseNotification("Сохранение сброшено");
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f13f1b81e87df7c2b1ff010eaf6752f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 39065b0841a33aca39ecd52d9e1ee3b9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+85
View File
@@ -0,0 +1,85 @@
using System;
using OTGIntegrated.Core;
using OTGIntegrated.Talents;
using UnityEngine;
namespace OTGIntegrated.Stats
{
public sealed class LevelSystem : MonoBehaviour
{
[Header("Progression")]
public int level = 1;
public int currentExp = 0;
public int expToNextLevel = 100;
private TalentManager talentManager;
public event Action OnExperienceChanged;
public event Action OnLevelChanged;
public void Initialize(TalentManager talentManager)
{
this.talentManager = talentManager;
level = Mathf.Max(1, level);
currentExp = Mathf.Max(0, currentExp);
expToNextLevel = Mathf.Max(1, expToNextLevel);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.X))
{
AddExperience(50);
}
}
public void AddExperience(int amount)
{
if (amount <= 0)
{
return;
}
currentExp += amount;
GameEvents.RaiseExperienceGained(amount);
GameEvents.RaiseNotification($"Получено опыта: {amount}");
while (currentExp >= expToNextLevel)
{
currentExp -= expToNextLevel;
LevelUp();
}
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);
}
GameEvents.RaiseLevelUp(level);
GameEvents.RaiseNotification($"Уровень повышен: {level}");
OnLevelChanged?.Invoke();
}
public float GetNormalizedExp()
{
return expToNextLevel <= 0 ? 0f : Mathf.Clamp01((float)currentExp / expToNextLevel);
}
public void SetProgress(int savedLevel, int savedExp, int savedExpToNext)
{
level = Mathf.Max(1, savedLevel);
currentExp = Mathf.Max(0, savedExp);
expToNextLevel = Mathf.Max(1, savedExpToNext);
OnExperienceChanged?.Invoke();
OnLevelChanged?.Invoke();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ebb4a0bf744265069f337938f0ae255
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+9
View File
@@ -0,0 +1,9 @@
namespace OTGIntegrated.Stats
{
public enum ModifierType
{
FlatAdd,
PercentAdd,
PercentMultiply
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b2240c0c0f7a697db9db9b7747af0b75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+325
View File
@@ -0,0 +1,325 @@
using System;
using System.Collections.Generic;
using OTGIntegrated.Core;
using UnityEngine;
namespace OTGIntegrated.Stats
{
public sealed 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 baseCriticalChance = 0f;
[SerializeField] private float baseCooldownReduction = 0f;
[Header("Runtime")]
[SerializeField] private float currentHealth = 100f;
[SerializeField] private float currentMana = 100f;
[SerializeField] private List<StatModifier> activeModifiers = new List<StatModifier>();
private readonly Dictionary<StatType, float> baseValues = new Dictionary<StatType, float>();
private readonly Dictionary<StatType, float> finalValues = new Dictionary<StatType, float>();
public event Action OnStatsChanged;
public float MaxHealth => GetStat(StatType.MaxHealth);
public float CurrentHealth => currentHealth;
public float MaxMana => GetStat(StatType.MaxMana);
public float CurrentMana => currentMana;
public float Damage => GetStat(StatType.Damage);
public float MoveSpeed => GetStat(StatType.MoveSpeed);
public float CriticalChance => GetStat(StatType.CriticalChance);
public float CooldownReduction => GetStat(StatType.CooldownReduction);
public bool IsAlive => currentHealth > 0f;
private void Awake()
{
RecalculateStats(false);
currentHealth = Mathf.Clamp(currentHealth <= 0f ? MaxHealth : currentHealth, 0f, MaxHealth);
currentMana = Mathf.Clamp(currentMana <= 0f ? MaxMana : currentMana, 0f, MaxMana);
}
private void OnValidate()
{
ClampBaseValues();
currentHealth = Mathf.Max(0f, currentHealth);
currentMana = Mathf.Max(0f, currentMana);
RecalculateStats(false);
}
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 (finalValues.Count == 0)
{
RecalculateStats(false);
}
return finalValues.TryGetValue(statType, out float value) ? value : GetBaseStat(statType);
}
public void AddModifier(StatModifier modifier)
{
if (!IsValidNumber(modifier.value))
{
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(bool notify = true)
{
float previousMaxHealth = finalValues.TryGetValue(StatType.MaxHealth, out float oldHealth) ? oldHealth : baseMaxHealth;
float previousMaxMana = finalValues.TryGetValue(StatType.MaxMana, out float oldMana) ? oldMana : baseMaxMana;
ClampBaseValues();
EnsureBaseValues();
finalValues.Clear();
foreach (StatType statType in Enum.GetValues(typeof(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 value = (GetBaseStat(statType) + flatAdd) * Mathf.Max(0f, 1f + percentAdd) * percentMultiply;
finalValues[statType] = ClampFinalValue(statType, value);
}
if (previousMaxHealth > 0f && MaxHealth > previousMaxHealth)
{
currentHealth += MaxHealth - previousMaxHealth;
}
if (previousMaxMana > 0f && MaxMana > previousMaxMana)
{
currentMana += MaxMana - previousMaxMana;
}
ClampVitals();
if (notify)
{
OnStatsChanged?.Invoke();
GameEvents.RaiseStatsChanged();
}
}
public void TakeDamage(float amount)
{
if (amount <= 0f)
{
return;
}
currentHealth = Mathf.Clamp(currentHealth - amount, 0f, MaxHealth);
RaiseChanged();
}
public void Heal(float amount)
{
if (amount <= 0f)
{
return;
}
currentHealth = Mathf.Clamp(currentHealth + amount, 0f, MaxHealth);
RaiseChanged();
}
public bool SpendMana(float amount)
{
if (amount <= 0f)
{
return true;
}
if (currentMana + 0.001f < amount)
{
GameEvents.RaiseNotification("Недостаточно маны");
return false;
}
currentMana = Mathf.Clamp(currentMana - amount, 0f, MaxMana);
RaiseChanged();
return true;
}
public void RestoreMana(float amount)
{
if (amount <= 0f)
{
return;
}
currentMana = Mathf.Clamp(currentMana + amount, 0f, MaxMana);
RaiseChanged();
}
public float GetNormalizedHealth()
{
return MaxHealth <= 0f ? 0f : Mathf.Clamp01(currentHealth / MaxHealth);
}
public float GetNormalizedMana()
{
return MaxMana <= 0f ? 0f : Mathf.Clamp01(currentMana / MaxMana);
}
public void SetRuntimeVitals(float health, float mana)
{
currentHealth = Mathf.Clamp(health, 0f, MaxHealth);
currentMana = Mathf.Clamp(mana, 0f, MaxMana);
RaiseChanged();
}
private void EnsureBaseValues()
{
baseValues[StatType.MaxHealth] = Mathf.Max(1f, 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.CriticalChance] = Mathf.Clamp01(baseCriticalChance);
baseValues[StatType.CooldownReduction] = Mathf.Clamp(baseCooldownReduction, 0f, 0.9f);
}
private void ClampBaseValues()
{
baseMaxHealth = Mathf.Max(1f, baseMaxHealth);
baseMaxMana = Mathf.Max(0f, baseMaxMana);
baseDamage = Mathf.Max(0f, baseDamage);
baseMoveSpeed = Mathf.Max(0.1f, baseMoveSpeed);
baseCriticalChance = Mathf.Clamp01(baseCriticalChance);
baseCooldownReduction = Mathf.Clamp(baseCooldownReduction, 0f, 0.9f);
}
private float ClampFinalValue(StatType statType, float value)
{
if (!IsValidNumber(value))
{
value = GetBaseStat(statType);
}
switch (statType)
{
case StatType.MaxHealth:
return Mathf.Max(1f, value);
case StatType.MaxMana:
case StatType.Damage:
return Mathf.Max(0f, value);
case StatType.MoveSpeed:
return Mathf.Max(0.1f, value);
case StatType.CriticalChance:
case StatType.CooldownReduction:
return Mathf.Clamp(value, 0f, 0.9f);
default:
return Mathf.Max(0f, value);
}
}
private void ClampVitals()
{
currentHealth = Mathf.Clamp(currentHealth, 0f, MaxHealth);
currentMana = Mathf.Clamp(currentMana, 0f, MaxMana);
}
private bool IsValidNumber(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value);
}
private void RaiseChanged()
{
OnStatsChanged?.Invoke();
GameEvents.RaiseStatsChanged();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3bb33a590d7a867f38d838d4dddb03d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+21
View File
@@ -0,0 +1,21 @@
using System;
namespace OTGIntegrated.Stats
{
[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: b6372c114a00623a6986e92fdcdf85ae
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+12
View File
@@ -0,0 +1,12 @@
namespace OTGIntegrated.Stats
{
public enum StatType
{
MaxHealth,
MaxMana,
Damage,
MoveSpeed,
CriticalChance,
CooldownReduction
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c29308e24c83bb7ab58c6a7fe9a7f75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5749693a9ddc0a442a3ddfa5381bfa0b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+33
View File
@@ -0,0 +1,33 @@
using OTGIntegrated.Stats;
using UnityEngine;
namespace OTGIntegrated.Talents
{
[CreateAssetMenu(fileName = "TalentData", menuName = "OTG Integrated/Talent Data")]
public sealed class TalentData : ScriptableObject
{
public string talentId;
public string displayName;
[TextArea(2, 5)] public string description;
public Sprite icon;
[Min(0)] public int cost = 1;
public TalentData[] prerequisites;
public StatModifier[] modifiers;
public Vector2 treePosition;
private void OnValidate()
{
if (string.IsNullOrWhiteSpace(talentId))
{
talentId = name;
}
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = talentId;
}
cost = Mathf.Max(0, cost);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fdead87a2f17105e3bcc7d1536c493c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+268
View File
@@ -0,0 +1,268 @@
using System;
using System.Collections.Generic;
using OTGIntegrated.Core;
using OTGIntegrated.Stats;
using UnityEngine;
namespace OTGIntegrated.Talents
{
public sealed class TalentManager : MonoBehaviour
{
private const string TalentSourcePrefix = "Talent:";
public List<TalentData> allTalents = new List<TalentData>();
public int availableTalentPoints;
[SerializeField] private List<string> learnedTalentIds = new List<string>();
private PlayerStats playerStats;
private readonly Dictionary<string, TalentData> talentLookup = new Dictionary<string, TalentData>(StringComparer.Ordinal);
public event Action OnTalentStateChanged;
public IReadOnlyList<string> LearnedTalentIds => learnedTalentIds;
public void Initialize(PlayerStats stats)
{
playerStats = stats;
BuildLookup();
RebuildAllModifiers();
RaiseChanged();
}
private void Awake()
{
if (playerStats == null)
{
playerStats = GetComponent<PlayerStats>();
}
BuildLookup();
}
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 bool LearnTalent(TalentData talent)
{
if (!IsValidTalent(talent))
{
GameEvents.RaiseNotification("Некорректный талант");
return false;
}
if (!CanLearn(talent))
{
GameEvents.RaiseNotification(GetBlockingReason(talent));
return false;
}
availableTalentPoints -= Mathf.Max(0, talent.cost);
learnedTalentIds.Add(talent.talentId);
ApplyTalentModifiers(talent);
GameEvents.RaiseTalentLearned(talent);
GameEvents.RaiseNotification($"Изучен талант: {talent.displayName}");
RaiseChanged();
return true;
}
public void AddTalentPoints(int amount)
{
if (amount <= 0)
{
return;
}
availableTalentPoints = Mathf.Max(0, availableTalentPoints + amount);
GameEvents.RaiseNotification(amount == 1 ? "Получено очко талантов" : $"Получено очков талантов: {amount}");
RaiseChanged();
}
public void SetTalentPoints(int points)
{
availableTalentPoints = Mathf.Max(0, points);
RaiseChanged();
}
public TalentData GetTalentById(string talentId)
{
BuildLookup();
if (string.IsNullOrWhiteSpace(talentId))
{
return null;
}
talentLookup.TryGetValue(talentId, out TalentData talent);
return talent;
}
public string GetBlockingReason(TalentData talent)
{
if (!IsValidTalent(talent))
{
return "Некорректные данные таланта";
}
if (IsLearned(talent))
{
return "Талант уже изучен";
}
if (!ArePrerequisitesLearned(talent))
{
return GetMissingPrerequisiteText(talent);
}
if (availableTalentPoints < Mathf.Max(0, talent.cost))
{
return "Недостаточно очков талантов";
}
return "Доступно";
}
public void LoadProgress(int points, List<string> learnedIds)
{
BuildLookup();
availableTalentPoints = Mathf.Max(0, points);
learnedTalentIds.Clear();
if (learnedIds != null)
{
for (int i = 0; i < learnedIds.Count; i++)
{
string id = learnedIds[i];
if (!string.IsNullOrWhiteSpace(id) && talentLookup.ContainsKey(id) && !learnedTalentIds.Contains(id))
{
learnedTalentIds.Add(id);
}
}
}
RebuildAllModifiers();
RaiseChanged();
}
public void ResetTalents()
{
learnedTalentIds.Clear();
availableTalentPoints = 0;
RebuildAllModifiers();
RaiseChanged();
}
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();
}
private void ApplyTalentModifiers(TalentData talent)
{
if (!IsValidTalent(talent) || playerStats == null || talent.modifiers == null)
{
return;
}
string sourceId = TalentSourcePrefix + talent.talentId;
playerStats.RemoveModifiersBySource(sourceId);
for (int i = 0; i < talent.modifiers.Length; i++)
{
StatModifier modifier = talent.modifiers[i];
modifier.sourceId = sourceId;
playerStats.AddModifier(modifier);
}
}
private void BuildLookup()
{
talentLookup.Clear();
for (int i = 0; i < allTalents.Count; i++)
{
TalentData talent = allTalents[i];
if (IsValidTalent(talent))
{
talentLookup[talent.talentId] = talent;
}
}
}
private bool IsValidTalent(TalentData talent)
{
return talent != null && !string.IsNullOrWhiteSpace(talent.talentId);
}
private string GetMissingPrerequisiteText(TalentData talent)
{
if (talent.prerequisites == null)
{
return "Требования не выполнены";
}
for (int i = 0; i < talent.prerequisites.Length; i++)
{
TalentData prerequisite = talent.prerequisites[i];
if (!IsValidTalent(prerequisite) || !IsLearned(prerequisite))
{
return prerequisite != null ? $"Требуется: {prerequisite.displayName}" : "Требуется другой талант";
}
}
return "Требования не выполнены";
}
private void RaiseChanged()
{
GameEvents.RaiseTalentPointChanged(availableTalentPoints);
OnTalentStateChanged?.Invoke();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d62f8065fb79868b590633754446f310
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+110
View File
@@ -0,0 +1,110 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace OTGIntegrated.Talents
{
public sealed class TalentNodeUI : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
public Image background;
public Text titleText;
public Text costText;
public Button button;
private TalentData talent;
private TalentManager manager;
private TalentTooltipUI tooltip;
public TalentData Talent => talent;
public void Initialize(TalentData talent, TalentManager manager, TalentTooltipUI tooltip, Font font)
{
this.talent = talent;
this.manager = manager;
this.tooltip = tooltip;
if (background == null) background = GetComponent<Image>();
if (button == null) button = GetComponent<Button>();
if (titleText != null)
{
titleText.font = font != null ? font : Resources.GetBuiltinResource<Font>("Arial.ttf");
}
if (costText != null)
{
costText.font = font != null ? font : Resources.GetBuiltinResource<Font>("Arial.ttf");
}
if (button != null)
{
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() =>
{
if (this.manager != null)
{
this.manager.LearnTalent(this.talent);
}
});
}
Refresh();
}
public void Refresh()
{
if (talent == null)
{
return;
}
if (titleText != null)
{
titleText.text = talent.displayName;
}
if (costText != null)
{
costText.text = talent.cost.ToString();
}
bool learned = manager != null && manager.IsLearned(talent);
bool canLearn = manager != null && manager.CanLearn(talent);
bool prereqReady = manager != null && manager.ArePrerequisitesLearned(talent);
if (background != null)
{
if (learned)
{
background.color = new Color(0.86f, 0.68f, 0.22f, 0.96f);
}
else if (canLearn)
{
background.color = new Color(0.2f, 0.48f, 0.72f, 0.94f);
}
else if (prereqReady)
{
background.color = new Color(0.18f, 0.2f, 0.26f, 0.92f);
}
else
{
background.color = new Color(0.09f, 0.1f, 0.12f, 0.88f);
}
}
if (button != null)
{
button.interactable = canLearn;
}
}
public void OnPointerEnter(PointerEventData eventData)
{
tooltip?.Show(talent, manager);
}
public void OnPointerExit(PointerEventData eventData)
{
tooltip?.Hide();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 49565b0125ddc9cec9f37602092608ad
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
using UnityEngine;
using UnityEngine.UI;
namespace OTGIntegrated.Talents
{
public sealed class TalentTooltipUI : MonoBehaviour
{
public GameObject panelRoot;
public Text contentText;
private void Awake()
{
if (panelRoot == null)
{
panelRoot = gameObject;
}
}
public void Show(TalentData talent, TalentManager manager)
{
if (talent == null || panelRoot == null || contentText == null)
{
return;
}
string requirement = manager != null ? manager.GetBlockingReason(talent) : string.Empty;
contentText.text = $"{talent.displayName}\n\n{talent.description}\n\nСтоимость: {talent.cost}\n{requirement}";
panelRoot.SetActive(true);
}
public void Hide()
{
if (panelRoot != null)
{
panelRoot.SetActive(false);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19522a16ab21edb628b5c527bb3211ba
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+231
View File
@@ -0,0 +1,231 @@
using System.Collections.Generic;
using OTGIntegrated.Save;
using OTGIntegrated.Stats;
using UnityEngine;
using UnityEngine.UI;
namespace OTGIntegrated.Talents
{
public sealed class TalentTreeUI : MonoBehaviour
{
public GameObject panelRoot;
public RectTransform nodesRoot;
public RectTransform lineRoot;
public TalentTooltipUI tooltip;
public Text titleText;
public Text pointsText;
public Text statsText;
public Button saveButton;
public Button loadButton;
public Button resetButton;
public Button closeButton;
public Font uiFont;
private TalentManager talentManager;
private PlayerStats playerStats;
private readonly List<TalentNodeUI> nodes = new List<TalentNodeUI>();
private readonly List<GameObject> spawnedLines = new List<GameObject>();
public void Initialize(TalentManager manager, PlayerStats stats)
{
talentManager = manager;
playerStats = stats;
if (talentManager != null)
{
talentManager.OnTalentStateChanged -= Refresh;
talentManager.OnTalentStateChanged += Refresh;
}
if (playerStats != null)
{
playerStats.OnStatsChanged -= Refresh;
playerStats.OnStatsChanged += Refresh;
}
if (saveButton != null) saveButton.onClick.AddListener(() => FindObjectOfType<SaveSystem>()?.Save());
if (loadButton != null) loadButton.onClick.AddListener(() => FindObjectOfType<SaveSystem>()?.Load());
if (resetButton != null) resetButton.onClick.AddListener(() => FindObjectOfType<SaveSystem>()?.ResetSave());
if (closeButton != null) closeButton.onClick.AddListener(() => FindObjectOfType<OTGIntegrated.UI.UIManager>()?.CloseCurrentWindow());
BuildTree();
Refresh();
}
private void OnEnable()
{
Refresh();
}
private void OnDestroy()
{
if (talentManager != null)
{
talentManager.OnTalentStateChanged -= Refresh;
}
if (playerStats != null)
{
playerStats.OnStatsChanged -= Refresh;
}
}
public void BuildTree()
{
ClearTree();
if (talentManager == null || nodesRoot == null)
{
return;
}
Dictionary<TalentData, TalentNodeUI> lookup = new Dictionary<TalentData, TalentNodeUI>();
for (int i = 0; i < talentManager.allTalents.Count; i++)
{
TalentData talent = talentManager.allTalents[i];
if (talent == null)
{
continue;
}
TalentNodeUI node = CreateNode(talent);
nodes.Add(node);
lookup[talent] = node;
}
for (int i = 0; i < talentManager.allTalents.Count; i++)
{
TalentData talent = talentManager.allTalents[i];
if (talent == null || talent.prerequisites == null || !lookup.TryGetValue(talent, out TalentNodeUI node))
{
continue;
}
for (int p = 0; p < talent.prerequisites.Length; p++)
{
TalentData prerequisite = talent.prerequisites[p];
if (prerequisite != null && lookup.TryGetValue(prerequisite, out TalentNodeUI prerequisiteNode))
{
CreateLine(prerequisiteNode.GetComponent<RectTransform>().anchoredPosition, node.GetComponent<RectTransform>().anchoredPosition);
}
}
}
}
public void Refresh()
{
if (titleText != null)
{
titleText.text = "Дерево талантов";
}
if (pointsText != null)
{
int points = talentManager != null ? talentManager.availableTalentPoints : 0;
pointsText.text = $"Очки талантов: {points}";
}
if (statsText != null)
{
statsText.text = playerStats == null
? "Характеристики недоступны"
: $"Характеристики\nHP: {Mathf.RoundToInt(playerStats.CurrentHealth)} / {Mathf.RoundToInt(playerStats.MaxHealth)}\nMana: {Mathf.RoundToInt(playerStats.CurrentMana)} / {Mathf.RoundToInt(playerStats.MaxMana)}\nDamage: {playerStats.Damage:0.#}\nMove: {playerStats.MoveSpeed:0.#}\nCrit: {playerStats.CriticalChance:P0}\nCooldown: {playerStats.CooldownReduction:P0}";
}
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i] != null)
{
nodes[i].Refresh();
}
}
}
private TalentNodeUI CreateNode(TalentData talent)
{
GameObject root = new GameObject(talent.displayName, typeof(RectTransform), typeof(Image), typeof(Button), typeof(TalentNodeUI));
root.transform.SetParent(nodesRoot, false);
RectTransform rect = root.GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(160f, 76f);
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = talent.treePosition;
Text title = CreateText(root.transform, "Title", talent.displayName, 15, TextAnchor.MiddleCenter);
title.rectTransform.anchorMin = new Vector2(0.08f, 0.25f);
title.rectTransform.anchorMax = new Vector2(0.92f, 0.9f);
title.rectTransform.offsetMin = Vector2.zero;
title.rectTransform.offsetMax = Vector2.zero;
Text cost = CreateText(root.transform, "Cost", talent.cost.ToString(), 14, TextAnchor.MiddleCenter);
cost.rectTransform.anchorMin = new Vector2(0.78f, 0.02f);
cost.rectTransform.anchorMax = new Vector2(0.98f, 0.3f);
cost.rectTransform.offsetMin = Vector2.zero;
cost.rectTransform.offsetMax = Vector2.zero;
TalentNodeUI node = root.GetComponent<TalentNodeUI>();
node.background = root.GetComponent<Image>();
node.button = root.GetComponent<Button>();
node.titleText = title;
node.costText = cost;
node.Initialize(talent, talentManager, tooltip, uiFont);
return node;
}
private void CreateLine(Vector2 start, Vector2 end)
{
if (lineRoot == null)
{
return;
}
GameObject line = new GameObject("TalentLine", typeof(RectTransform), typeof(Image));
line.transform.SetParent(lineRoot, false);
spawnedLines.Add(line);
Image image = line.GetComponent<Image>();
image.color = new Color(0.78f, 0.62f, 0.26f, 0.55f);
RectTransform rect = line.GetComponent<RectTransform>();
Vector2 delta = end - start;
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.sizeDelta = new Vector2(delta.magnitude, 3f);
rect.anchoredPosition = start + delta * 0.5f;
rect.localRotation = Quaternion.Euler(0f, 0f, Mathf.Atan2(delta.y, delta.x) * Mathf.Rad2Deg);
}
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.95f, 0.94f, 0.9f, 1f);
text.text = value;
text.resizeTextForBestFit = true;
text.resizeTextMinSize = 10;
text.resizeTextMaxSize = fontSize;
return text;
}
private void ClearTree()
{
for (int i = nodesRoot != null ? nodesRoot.childCount - 1 : -1; i >= 0; i--)
{
Destroy(nodesRoot.GetChild(i).gameObject);
}
for (int i = 0; i < spawnedLines.Count; i++)
{
if (spawnedLines[i] != null)
{
Destroy(spawnedLines[i]);
}
}
nodes.Clear();
spawnedLines.Clear();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e51febd0e78f304ea44f20db69a102f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3fac2e158a74e5de9906af74c73df48b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+171
View File
@@ -0,0 +1,171 @@
using System;
using OTGIntegrated.Abilities;
using OTGIntegrated.Core;
using OTGIntegrated.Quests;
using OTGIntegrated.Stats;
using OTGIntegrated.Weapons;
using UnityEngine;
using UnityEngine.UI;
namespace OTGIntegrated.UI
{
[Serializable]
public sealed class AbilityHudSlot
{
public Text keyText;
public Text nameText;
public Text manaText;
public Text cooldownText;
public Image cooldownOverlay;
public Image iconImage;
}
public sealed class HUDController : MonoBehaviour
{
[Header("Status")]
public Image hpFill;
public Image manaFill;
public Image expFill;
public Text hpText;
public Text manaText;
public Text expText;
public Text levelText;
[Header("Quest")]
public Text questTitleText;
public Text questBodyText;
[Header("Abilities")]
public AbilityHudSlot[] abilitySlots = new AbilityHudSlot[3];
[Header("Weapon")]
public Text weaponNameText;
public Text weaponDamageText;
public Text weaponHintText;
private PlayerStats stats;
private LevelSystem levelSystem;
private QuestManager questManager;
private AbilityManager abilityManager;
private WeaponManager weaponManager;
public void Initialize(PlayerStats stats, LevelSystem levelSystem, QuestManager questManager, AbilityManager abilityManager, WeaponManager weaponManager)
{
this.stats = stats;
this.levelSystem = levelSystem;
this.questManager = questManager;
this.abilityManager = abilityManager;
this.weaponManager = weaponManager;
if (stats != null) stats.OnStatsChanged += RefreshAll;
if (levelSystem != null)
{
levelSystem.OnExperienceChanged += RefreshAll;
levelSystem.OnLevelChanged += RefreshAll;
}
if (questManager != null) questManager.OnQuestsChanged += RefreshAll;
if (abilityManager != null) abilityManager.OnAbilitiesChanged += RefreshAbilities;
GameEvents.OnWeaponChanged += _ => RefreshWeapon();
GameEvents.OnStatsChanged += RefreshAll;
RefreshAll();
}
private void OnDestroy()
{
if (stats != null) stats.OnStatsChanged -= RefreshAll;
if (levelSystem != null)
{
levelSystem.OnExperienceChanged -= RefreshAll;
levelSystem.OnLevelChanged -= RefreshAll;
}
if (questManager != null) questManager.OnQuestsChanged -= RefreshAll;
if (abilityManager != null) abilityManager.OnAbilitiesChanged -= RefreshAbilities;
GameEvents.OnStatsChanged -= RefreshAll;
}
private void Update()
{
RefreshAbilities();
}
public void RefreshAll()
{
RefreshStatus();
RefreshQuest();
RefreshAbilities();
RefreshWeapon();
}
private void RefreshStatus()
{
if (stats != null)
{
if (hpFill != null) hpFill.fillAmount = stats.GetNormalizedHealth();
if (manaFill != null) manaFill.fillAmount = stats.GetNormalizedMana();
if (hpText != null) hpText.text = $"HP {Mathf.RoundToInt(stats.CurrentHealth)} / {Mathf.RoundToInt(stats.MaxHealth)}";
if (manaText != null) manaText.text = $"Mana {Mathf.RoundToInt(stats.CurrentMana)} / {Mathf.RoundToInt(stats.MaxMana)}";
}
if (levelSystem != null)
{
if (expFill != null) expFill.fillAmount = levelSystem.GetNormalizedExp();
if (expText != null) expText.text = $"EXP {levelSystem.currentExp} / {levelSystem.expToNextLevel}";
if (levelText != null) levelText.text = $"Level {levelSystem.level}";
}
}
private void RefreshQuest()
{
if (questTitleText != null)
{
questTitleText.text = "Активная цель";
}
if (questBodyText != null)
{
questBodyText.text = questManager != null ? questManager.GetPrimaryTrackerText() : "Нет активных квестов";
}
}
private void RefreshAbilities()
{
if (abilityManager == null || abilitySlots == null)
{
return;
}
for (int i = 0; i < abilitySlots.Length; i++)
{
AbilityHudSlot slot = abilitySlots[i];
if (slot == null)
{
continue;
}
AbilityData ability = i < abilityManager.LearnedAbilities.Count ? abilityManager.LearnedAbilities[i] : null;
float remaining = abilityManager.GetCooldownRemaining(i);
if (slot.keyText != null) slot.keyText.text = (i + 1).ToString();
if (slot.nameText != null) slot.nameText.text = ability != null ? ability.displayName : "-";
if (slot.manaText != null) slot.manaText.text = ability != null ? Mathf.RoundToInt(ability.manaCost).ToString() : string.Empty;
if (slot.cooldownOverlay != null) slot.cooldownOverlay.fillAmount = abilityManager.GetCooldownNormalized(i);
if (slot.cooldownText != null) slot.cooldownText.text = remaining > 0.05f ? remaining.ToString("0.0") : string.Empty;
if (slot.iconImage != null)
{
slot.iconImage.sprite = ability != null ? ability.icon : null;
slot.iconImage.color = ability != null && ability.icon != null ? Color.white : new Color(0.2f, 0.22f, 0.26f, 1f);
}
}
}
private void RefreshWeapon()
{
WeaponData weapon = weaponManager != null ? weaponManager.CurrentWeapon : null;
if (weaponNameText != null) weaponNameText.text = weapon != null ? $"Weapon: {weapon.displayName}" : "Weapon: -";
if (weaponDamageText != null) weaponDamageText.text = weaponManager != null ? $"Damage: {weaponManager.CurrentFinalDamage:0}" : "Damage: -";
if (weaponHintText != null) weaponHintText.text = "LMB — Attack";
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4d32dbffc8851c0e5a61fa47ffffdb2b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+40
View File
@@ -0,0 +1,40 @@
using UnityEngine;
using UnityEngine.UI;
namespace OTGIntegrated.UI
{
public sealed class InteractionPromptUI : MonoBehaviour
{
public GameObject root;
public Text promptText;
private void Awake()
{
if (root == null)
{
root = gameObject;
}
}
public void Show(string prompt)
{
if (root != null)
{
root.SetActive(true);
}
if (promptText != null)
{
promptText.text = prompt;
}
}
public void Hide()
{
if (root != null)
{
root.SetActive(false);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More