Files
beyond/Assets/Scripts/Characters/Skills/MagicAttacks.cs

292 lines
11 KiB
C#

using System.Collections;
using System.Collections.Generic;
using Invector;
using Invector.vMelee;
using Invector.vCharacterController;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Events;
using DG.Tweening;
using Beyond;
namespace Beyond
{
[RequireComponent(typeof(Animator))]
public class MagicAttacks : MonoBehaviour
{
[Title("References")]
[SerializeField] private bEquipArea powersArea;
[Title("Input Settings")]
public GenericInput actionInput = new GenericInput("L", "L", "L");
[Title("Events")]
public UnityEvent OnPlayAnimation;
public UnityEvent OnEndAnimation;
public event System.Action<SpellDefinition, Collider> OnSpellHit;
// Internal State
private bThirdPersonInput tpInput;
private AutoTargetting _autoTargettingInstance;
public bool isPlaying { get; private set; }
private bool canPlayNoFaithClip = true;
[field: Sirenix.OdinInspector.ShowInInspector, Sirenix.OdinInspector.ReadOnly]
public bool IsShieldActive { get; set; }
// Constants
private const int spellLayerIndex = 5;
private void Awake()
{
tpInput = GetComponent<bThirdPersonInput>();
if (Player.Instance != null)
{
_autoTargettingInstance = Player.Instance.AutoTarget;
}
}
private void OnDisable()
{
// Safety check: ensure shield flag is reset if player is disabled/respawning
IsShieldActive = false;
}
private void LateUpdate()
{
HandleInput();
MonitorAnimationState();
}
private void HandleInput()
{
// 1. Basic Checks
if (tpInput == null || tpInput.cc == null) return;
bool canCast = !isPlaying &&
!tpInput.cc.customAction &&
!tpInput.cc.IsAnimatorTag("special") &&
!tpInput.cc.IsAnimatorTag("LockMovement");
// 2. Trigger
if (actionInput.GetButtonDown() && canCast)
{
TryCastCurrentSpell();
}
}
private void TryCastCurrentSpell()
{
// A. Get Item & Spell Definition
bItem selectedItem = GetEquippedSpellItem();
if (selectedItem == null)
{
// No item equipped in the selected slot
return;
}
if (selectedItem.spellDefinition == null)
{
Debug.LogWarning($"Item '{selectedItem.name}' is equipped but has no SpellDefinition assigned!");
return;
}
SpellDefinition spell = selectedItem.spellDefinition;
// B. Check Costs (Calculated by the Spell SO)
float cost = spell.GetFaithCost(Player.Instance);
if (Player.Instance.GetCurrentFaithValue() < cost)
{
PlayNoFaithFeedback();
return;
}
// C. Success! Consume Resources
Player.Instance.UpdateFaithCurrentValue(-cost);
// Effect: Growth (Heal on Cast) - Global Trinket logic
if (Player.Instance.CurrentTrinketStats.effectGrowth)
{
int healAmt = Mathf.Max(1, (int)(Player.Instance.MaxHealth * 0.02f));
Player.Instance.ThirdPersonController.ChangeHealth(healAmt);
}
// Handle Consumables (Scrolls)
if (selectedItem.destroyAfterUse)
{
// Uses the standard Invector logic to consume the item in the current slot
if (powersArea.currentSelectedSlot != null)
{
powersArea.UseItem(powersArea.currentSelectedSlot);
}
}
// D. Snap Rotation (Instant snap before animation starts if needed)
if (_autoTargettingInstance != null && _autoTargettingInstance.CurrentTarget != null)
{
SnapLookTowardsAutoTarget();
}
// E. Play Animation
if (!string.IsNullOrEmpty(spell.animationClipName))
{
tpInput.cc.animator.CrossFadeInFixedTime(spell.animationClipName, 0.1f);
OnPlayAnimation.Invoke();
}
// F. Execute Spell Logic
// We pass 'this' (Monobehaviour) so the SO can start coroutines here.
Transform target = _autoTargettingInstance != null ?
(_autoTargettingInstance.CurrentTarget != null ? _autoTargettingInstance.CurrentTarget.transform : null)
: null;
spell.Cast(this, target);
}
// =================================================================================================
// HELPER METHODS (Called by SpellDefinitions)
// =================================================================================================
/// <summary>
/// Gets the currently equipped item in the Powers area.
/// </summary>
public bItem GetEquippedSpellItem()
{
if (powersArea == null) return null;
return powersArea.currentEquippedItem;
}
/// <summary>
/// Coroutine used by Spells to rotate the player towards the target over time.
/// </summary>
public IEnumerator RotateTowardsTargetRoutine(Transform target, float duration)
{
if (duration <= 0) yield break;
float timer = 0f;
while (timer < duration)
{
// Dynamic check: target might die or become null during rotation
Transform actualTarget = target;
// Fallback to AutoTarget if the passed target becomes null but a new one exists
if (actualTarget == null && _autoTargettingInstance != null && _autoTargettingInstance.CurrentTarget != null)
{
actualTarget = _autoTargettingInstance.CurrentTarget.transform;
}
if (actualTarget != null)
{
Vector3 directionToTarget = actualTarget.position - transform.position;
directionToTarget.y = 0f;
if (directionToTarget.sqrMagnitude > 0.0001f)
{
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized);
// Using a high speed to ensure we catch up, or use the AutoTargetting speed settings
float speed = (_autoTargettingInstance != null) ? _autoTargettingInstance.playerRotationSpeed : 10f;
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, Time.deltaTime * speed * 50f);
}
}
timer += Time.deltaTime;
yield return null;
}
}
/// <summary>
/// Helper to apply Trinket Damage Multipliers (Soulfire) to any instantiated spell object.
/// </summary>
public void ApplyDamageModifiers(GameObject spellObject)
{
if (spellObject == null || Player.Instance == null) return;
float mult = Player.Instance.CurrentTrinketStats.soulfireDamageMult;
// Only apply if multiplier is significant (not 1.0)
if (Mathf.Abs(mult - 1f) > 0.01f)
{
var damageComps = spellObject.GetComponentsInChildren<vObjectDamage>();
foreach (var comp in damageComps)
{
comp.damage.damageValue = Mathf.RoundToInt(comp.damage.damageValue * mult);
}
}
}
// =================================================================================================
// INTERNAL LOGIC
// =================================================================================================
private void MonitorAnimationState()
{
if (tpInput == null || tpInput.cc == null || tpInput.cc.animator == null) return;
// Check if we are currently playing a spell animation on the specific layer
var stateInfo = tpInput.cc.animator.GetCurrentAnimatorStateInfo(spellLayerIndex);
// We consider it "Playing" if the tag is Spell, or if a spell animation is active
// Note: Your SpellDefinitions define the Clip Name.
// A generic way is checking the Tag if you set "Spell" tag in Animator,
// OR checking if we are in a transition to a spell.
// Simplified check based on your old code logic:
// Assuming all Spell Animations have the tag "Spell" or "Action" in the Animator
isPlaying = tpInput.cc.IsAnimatorTag("Spell") ||
tpInput.cc.IsAnimatorTag("special") ||
tpInput.cc.customAction;
// Optional: Trigger OnEndAnimation if needed, though most logic is now in the Coroutine
}
private void SnapLookTowardsAutoTarget()
{
if (_autoTargettingInstance == null || _autoTargettingInstance.CurrentTarget == null) return;
Transform target = _autoTargettingInstance.CurrentTarget.transform;
Vector3 direction = target.position - transform.position;
direction.y = 0f;
if (direction.sqrMagnitude > 0.001f)
{
transform.rotation = Quaternion.LookRotation(direction.normalized);
}
}
private void PlayNoFaithFeedback()
{
if (!canPlayNoFaithClip) return;
canPlayNoFaithClip = false;
// Feedback
if (bItemCollectionDisplay.Instance != null)
bItemCollectionDisplay.Instance.FadeText("Not enough Faith", 4, 0.25f);
if (Player.Instance != null)
Player.Instance.PlayNoFaithClip();
// Reset cooldown
DOVirtual.DelayedCall(1.5f, () => canPlayNoFaithClip = true);
}
public void RegisterSpellDamageEvents(GameObject spellInstance, SpellDefinition spellDef)
{
if (spellInstance == null) return;
// Find all damage components on the spawned object
var damageComps = spellInstance.GetComponentsInChildren<Invector.vObjectDamage>();
foreach (var comp in damageComps)
{
// Invector's vObjectDamage event gives us the Collider directly
comp.onHit.AddListener((Collider hitCollider) =>
{
// Forward the event to anyone listening to MagicAttacks
OnSpellHit?.Invoke(spellDef, hitCollider);
});
}
}
}
}