first commit

This commit is contained in:
2026-06-04 23:22:13 +03:00
commit d329f501be
310 changed files with 36395 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
using UnityEngine;
namespace OTGIntegrated.Abilities
{
[CreateAssetMenu(fileName = "AbilityData", menuName = "OTG Integrated/Ability Data")]
public sealed class AbilityData : ScriptableObject
{
public string abilityId;
public string displayName;
[TextArea(2, 5)] public string description;
public float manaCost = 10f;
public float cooldown = 2f;
public float damage = 10f;
public float range = 8f;
public GameObject effectPrefab;
public Sprite icon;
private void OnValidate()
{
if (string.IsNullOrWhiteSpace(abilityId))
{
abilityId = name;
}
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = abilityId;
}
manaCost = Mathf.Max(0f, manaCost);
cooldown = Mathf.Max(0f, cooldown);
damage = Mathf.Max(0f, damage);
range = Mathf.Max(0f, range);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ad890eb3c985db91b32d80f01945dec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
using OTGIntegrated.Stats;
using UnityEngine;
namespace OTGIntegrated.Abilities
{
public readonly struct AbilityContext
{
public readonly AbilityData Ability;
public readonly Transform Caster;
public readonly PlayerStats Stats;
public readonly Vector3 Origin;
public readonly Vector3 Forward;
public AbilityContext(AbilityData ability, Transform caster, PlayerStats stats, Vector3 origin, Vector3 forward)
{
Ability = ability;
Caster = caster;
Stats = stats;
Origin = origin;
Forward = forward.sqrMagnitude > 0.001f ? forward.normalized : Vector3.forward;
}
}
public abstract class AbilityEffect : MonoBehaviour
{
public abstract void Activate(AbilityContext context);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac1934c301c9feb07b0a0e29236e916f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+181
View File
@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using OTGIntegrated.Core;
using OTGIntegrated.Player;
using OTGIntegrated.Stats;
using UnityEngine;
namespace OTGIntegrated.Abilities
{
public sealed class AbilityManager : MonoBehaviour
{
public List<AbilityData> learnedAbilities = new List<AbilityData>();
public Transform castOrigin;
private readonly List<float> cooldownRemaining = new List<float>();
private PlayerStats playerStats;
private PlayerController playerController;
public event Action OnAbilitiesChanged;
public IReadOnlyList<AbilityData> LearnedAbilities => learnedAbilities;
public void Initialize(PlayerStats stats)
{
playerStats = stats != null ? stats : GetComponent<PlayerStats>();
playerController = GetComponent<PlayerController>();
if (castOrigin == null)
{
castOrigin = transform;
}
EnsureCooldownSlots();
OnAbilitiesChanged?.Invoke();
}
private void Awake()
{
if (playerStats == null)
{
playerStats = GetComponent<PlayerStats>();
}
playerController = GetComponent<PlayerController>();
EnsureCooldownSlots();
}
private void Update()
{
TickCooldowns();
if (playerController != null && !playerController.InputEnabled)
{
return;
}
if (Input.GetKeyDown(KeyCode.Alpha1)) TryUseAbility(0);
if (Input.GetKeyDown(KeyCode.Alpha2)) TryUseAbility(1);
if (Input.GetKeyDown(KeyCode.Alpha3)) TryUseAbility(2);
}
public bool TryUseAbility(int index)
{
EnsureCooldownSlots();
if (index < 0 || index >= learnedAbilities.Count)
{
return false;
}
AbilityData ability = learnedAbilities[index];
if (ability == null || playerStats == null)
{
return false;
}
if (cooldownRemaining[index] > 0f)
{
GameEvents.RaiseNotification("Способность на перезарядке");
return false;
}
if (!playerStats.SpendMana(ability.manaCost))
{
return false;
}
if (ability.effectPrefab == null)
{
playerStats.RestoreMana(ability.manaCost);
Debug.LogWarning($"Ability has no effect prefab: {ability.displayName}", ability);
return false;
}
Vector3 forward = GetCastForward();
Vector3 origin = castOrigin != null ? castOrigin.position : transform.position + Vector3.up;
GameObject effectObject = Instantiate(ability.effectPrefab, origin, Quaternion.LookRotation(forward, Vector3.up));
AbilityEffect effect = effectObject.GetComponent<AbilityEffect>();
if (effect == null)
{
playerStats.RestoreMana(ability.manaCost);
Destroy(effectObject);
Debug.LogWarning($"Ability effect prefab misses AbilityEffect: {ability.effectPrefab.name}", ability.effectPrefab);
return false;
}
effect.Activate(new AbilityContext(ability, transform, playerStats, origin, forward));
cooldownRemaining[index] = GetEffectiveCooldown(ability);
GameEvents.RaiseAbilityUsed(ability);
GameEvents.RaiseNotification($"Способность: {ability.displayName}");
OnAbilitiesChanged?.Invoke();
return true;
}
public float GetCooldownRemaining(int index)
{
EnsureCooldownSlots();
return index >= 0 && index < cooldownRemaining.Count ? cooldownRemaining[index] : 0f;
}
public float GetCooldownNormalized(int index)
{
if (index < 0 || index >= learnedAbilities.Count || learnedAbilities[index] == null)
{
return 0f;
}
float cooldown = GetEffectiveCooldown(learnedAbilities[index]);
return cooldown <= 0f ? 0f : Mathf.Clamp01(GetCooldownRemaining(index) / cooldown);
}
private float GetEffectiveCooldown(AbilityData ability)
{
float reduction = playerStats != null ? playerStats.CooldownReduction : 0f;
return Mathf.Max(0f, ability.cooldown * (1f - Mathf.Clamp(reduction, 0f, 0.9f)));
}
private void TickCooldowns()
{
EnsureCooldownSlots();
bool changed = false;
for (int i = 0; i < cooldownRemaining.Count; i++)
{
if (cooldownRemaining[i] <= 0f)
{
continue;
}
cooldownRemaining[i] = Mathf.Max(0f, cooldownRemaining[i] - Time.deltaTime);
changed = true;
}
if (changed)
{
OnAbilitiesChanged?.Invoke();
}
}
private void EnsureCooldownSlots()
{
while (cooldownRemaining.Count < learnedAbilities.Count)
{
cooldownRemaining.Add(0f);
}
while (cooldownRemaining.Count > learnedAbilities.Count)
{
cooldownRemaining.RemoveAt(cooldownRemaining.Count - 1);
}
}
private Vector3 GetCastForward()
{
if (playerController != null)
{
return playerController.GetAimForward();
}
Vector3 forward = transform.forward;
forward.y = 0f;
return forward.sqrMagnitude > 0.001f ? forward.normalized : Vector3.forward;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af4f4f6d82dfc81a6b41ef63d2c4fef0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+43
View File
@@ -0,0 +1,43 @@
using UnityEngine;
namespace OTGIntegrated.Abilities
{
public sealed class BlinkEffect : AbilityEffect
{
public LayerMask obstacleMask = ~0;
public float collisionRadius = 0.45f;
public override void Activate(AbilityContext context)
{
if (context.Caster == null)
{
Destroy(gameObject);
return;
}
Vector3 origin = context.Caster.position + Vector3.up * 0.8f;
Vector3 direction = context.Forward;
float distance = Mathf.Max(0f, context.Ability.range);
if (Physics.SphereCast(origin, collisionRadius, direction, out RaycastHit hit, distance, obstacleMask, QueryTriggerInteraction.Ignore))
{
distance = Mathf.Max(0f, hit.distance - collisionRadius);
}
Vector3 target = context.Caster.position + direction * distance;
CharacterController controller = context.Caster.GetComponent<CharacterController>();
if (controller != null)
{
controller.enabled = false;
context.Caster.position = target;
controller.enabled = true;
}
else
{
context.Caster.position = target;
}
Destroy(gameObject, 0.15f);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e0a3dce724dee68bd9503fe7a80a80b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,81 @@
using OTGIntegrated.Combat;
using UnityEngine;
namespace OTGIntegrated.Abilities
{
[RequireComponent(typeof(Collider))]
public sealed class FireballEffect : AbilityEffect
{
public float speed = 16f;
public float radius = 0.35f;
public LayerMask hitMask = ~0;
private AbilityContext context;
private Vector3 direction;
private float remainingLife;
private bool active;
private void Awake()
{
Collider ownCollider = GetComponent<Collider>();
ownCollider.isTrigger = true;
}
public override void Activate(AbilityContext context)
{
this.context = context;
direction = context.Forward;
remainingLife = Mathf.Max(0.5f, context.Ability.range / Mathf.Max(1f, speed));
active = true;
transform.position = context.Origin + direction * 0.9f;
transform.rotation = Quaternion.LookRotation(direction, Vector3.up);
}
private void Update()
{
if (!active)
{
return;
}
float step = speed * Time.deltaTime;
if (Physics.SphereCast(transform.position, radius, direction, out RaycastHit hit, step, hitMask, QueryTriggerInteraction.Ignore))
{
ApplyHit(hit.collider);
return;
}
transform.position += direction * step;
remainingLife -= Time.deltaTime;
if (remainingLife <= 0f)
{
Destroy(gameObject);
}
}
private void OnTriggerEnter(Collider other)
{
if (active)
{
ApplyHit(other);
}
}
private void ApplyHit(Collider hitCollider)
{
if (hitCollider == null || context.Caster == null || hitCollider.transform == context.Caster || hitCollider.GetComponentInParent<AbilityEffect>() != null)
{
return;
}
Damageable damageable = hitCollider.GetComponentInParent<Damageable>();
if (damageable != null && damageable.IsAlive)
{
float finalDamage = context.Ability.damage + (context.Stats != null ? context.Stats.Damage : 0f);
damageable.TakeDamage(finalDamage, context.Caster.gameObject);
}
Destroy(gameObject);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fa6f20c109c09c3d99226f53d98d4bb7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+19
View File
@@ -0,0 +1,19 @@
using OTGIntegrated.Combat;
using UnityEngine;
namespace OTGIntegrated.Abilities
{
public sealed class HealEffect : AbilityEffect
{
public override void Activate(AbilityContext context)
{
if (context.Stats != null)
{
context.Stats.Heal(context.Ability.damage);
FloatingDamage.Spawn(context.Stats.transform.position + Vector3.up * 1.7f, Mathf.RoundToInt(context.Ability.damage), new Color(0.35f, 1f, 0.45f, 1f));
}
Destroy(gameObject, 0.1f);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: acee80077d1ce3be382de5f654f93f2b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: