first commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c9ca1efd428be9728a91e7626205769
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f694ab5867017989da700c10769b7ef2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cc71487b17c578579e59895008f7bb4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e203683fe758dec587d4bddb4347392
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81836fcfb7483bbb086ffd8384723a9d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c92f8d4902c2f63bbb7a3c47c875a94b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19f1587ed61214d2181a2d440a8b2922
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f2542de47c4a49c5b22548156ec5afa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ac0e9b4de9dd725f85d3f39f1b6ffa1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23725abb957f3f8c089b027d1a056689
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c0053840eae6c9b288b0745993dfd14
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 078fc01ab57931f239a827eff5dce814
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 662778782848d943092b5de50a866096
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a277f00581ad6576a0d5b045321746e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0acf6bd5ac3233ec49f28bd9ee809944
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12fa02eac54ea59fca4b6c5a2c604992
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 "Цель";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67df6628261903cac9131cc30928ae5b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61452d00fee61482aa5fb3d1fda042cf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7af1ab916f75fd9de86085a622817517
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88679d8b286ea9b998dee5138457108f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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("Сохранение сброшено");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f13f1b81e87df7c2b1ff010eaf6752f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39065b0841a33aca39ecd52d9e1ee3b9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ebb4a0bf744265069f337938f0ae255
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3bb33a590d7a867f38d838d4dddb03d9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace OTGIntegrated.Stats
|
||||
{
|
||||
public enum StatType
|
||||
{
|
||||
MaxHealth,
|
||||
MaxMana,
|
||||
Damage,
|
||||
MoveSpeed,
|
||||
CriticalChance,
|
||||
CooldownReduction
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c29308e24c83bb7ab58c6a7fe9a7f75
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5749693a9ddc0a442a3ddfa5381bfa0b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fac2e158a74e5de9906af74c73df48b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d32dbffc8851c0e5a61fa47ffffdb2b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
Reference in New Issue
Block a user