first commit
This commit is contained in:
@@ -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:
|
||||
Reference in New Issue
Block a user