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 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(); 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) // ================================================================================================= /// /// Gets the currently equipped item in the Powers area. /// public bItem GetEquippedSpellItem() { if (powersArea == null) return null; return powersArea.currentEquippedItem; } /// /// Coroutine used by Spells to rotate the player towards the target over time. /// 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; } } /// /// Helper to apply Trinket Damage Multipliers (Soulfire) to any instantiated spell object. /// 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(); 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(); 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); }); } } } }