using System; using System.Collections; using System.Collections.Generic; using Invector; using Invector.vCharacterController; using Invector.vCharacterController.vActions; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.Events; using UnityEngine.VFX; using DG.Tweening; using Invector.vCharacterController.AI.FSMBehaviour; using Beyond; namespace Beyond { [RequireComponent(typeof(Animator))] [RequireComponent(typeof(vGenericAnimation))] public class MagicAttacks : MonoBehaviour { public delegate void ActionDelegate(); [Serializable] public class EffectDesc { public string name = "MagicPush"; public string secondaryName = "MagicPush Scroll"; public float delay = 0f; public float startTime = 0f; public float endTime = 0f; public GameObject effectObject; public string animClipName = "MagicPush"; public ActionDelegate del; } [SerializeField] private bEquipArea powersArea; public List m_effects; public EffectDesc selectedEffect; public enum EffectType { MAGIC_PUSH, FLAME_THROWER, SCAN, NOONE, FIREBALL, SHIELD, SILENT_PEEK }; public EffectType m_selectedType = EffectType.NOONE; public string currentSelectedSpellName = ""; private int currentSpellFaithCost = int.MaxValue; private int equipAreaSelectedIndex = 0; private Coroutine lastPushRoutine = null; private ParticleSystem flame; private BoxCollider flameDamager; private bLockOn lockOn; private const float fireballAimerThreshold = -1.0f; private const float fireballAimerHeightAdjuster = 0.1f; private const float fireballDamagerDuration = 0.3f; private const float fireballTargetYPositionOffset = 0.75f; private const int spellLayerIndex = 5; private EffectDesc shield; private EffectDesc silentPeek; private ShieldEffectController shieldEffectController; private ShieldCollisionController shieldCollisionController; private bool canPlayNoFaithClip = true; private bool canPlayCantDoClip = true; private AutoTargetting _autoTargettingInstance; public UnityAction onHitFireball; // Animation State public bool isPlaying; protected bool triggerOnce; protected bThirdPersonInput tpInput; internal bool shieldEffectIsActive; private bool shieldAnimationIsActive; [Tooltip("Input to trigger the custom animation")] public GenericInput actionInput = new GenericInput("L", "L", "L"); [Tooltip("Name of the animation clip")] public string animationClip; [Tooltip("Where in the end of the animation will trigger the event OnEndAnimation")] public float animationEnd = 0.8f; public UnityEvent OnPlayAnimation; public UnityEvent OnEndAnimation; private void Awake() { tpInput = GetComponent(); lockOn = GetComponent(); if (Player.Instance != null) { _autoTargettingInstance = Player.Instance.AutoTarget; if (_autoTargettingInstance == null) { Debug.LogWarning("MagicAttacks: AutoTargetting component not found on Player.Instance.AutoTarget."); } } EffectDesc mpush = m_effects[(int)EffectType.MAGIC_PUSH]; EffectDesc flameThrowe = m_effects[(int)EffectType.FLAME_THROWER]; EffectDesc scan = m_effects[(int)EffectType.SCAN]; EffectDesc fireball = m_effects[(int)EffectType.FIREBALL]; shield = m_effects[(int)EffectType.SHIELD]; silentPeek = m_effects[(int)EffectType.SILENT_PEEK]; // Setup References if (shield.effectObject) { shieldEffectController = shield.effectObject.GetComponent(); shieldCollisionController = shield.effectObject.GetComponentInChildren(); shield.effectObject.SetActive(false); } if (mpush.effectObject != null) { mpush.effectObject.SetActive(false); } mpush.del = MagicPushAttack; if (flameThrowe.effectObject) { flameThrowe.effectObject.GetComponent().Stop(); var ps = flameThrowe.effectObject.GetComponentsInChildren(); foreach (var p in ps) p.Stop(); flame = flameThrowe.effectObject.GetComponent(); flameDamager = flameThrowe.effectObject.GetComponentInChildren(); } flameThrowe.del = FlameThrowerAttack; scan.del = Scan; if (scan.effectObject) scan.effectObject.SetActive(false); fireball.del = Fireball; if (fireball.effectObject) fireball.effectObject.SetActive(false); shield.del = Shield; silentPeek.del = OnSilentPeek; } private void OnDisable() { if (shieldAnimationIsActive) { DisableShield(); } } private void OnEnable() { canPlayNoFaithClip = true; canPlayCantDoClip = true; } protected virtual void LateUpdate() { TriggerSpellAnimation(); AnimationBehaviour(); } protected virtual void TriggerSpellAnimation() { bool playConditions = !isPlaying && tpInput != null && tpInput.cc != null && !(tpInput.cc.customAction || tpInput.cc.IsAnimatorTag("special") || tpInput.cc.IsAnimatorTag("LockMovement")); if (actionInput.GetButtonDown() && playConditions) TryToPlaySpellAnimation(); } // ---------------------------------------------------------------------------------- // CORE CASTING LOGIC (COSTS & BONUSES) // ---------------------------------------------------------------------------------- public void TryToPlaySpellAnimation() { selectedEffect = GetCurrentlySelectedPower(); if (selectedEffect == shield && shieldAnimationIsActive) { TryToPlayCantDoThatYetClip(); return; } if (Player.Instance == null) { Debug.LogError("Player.Instance is null. Cannot cast spell."); return; } // --- 1. CALCULATE FINAL FAITH COST --- float finalCost = currentSpellFaithCost; bool isSilentPeek = selectedEffect == m_effects[(int)EffectType.SILENT_PEEK]; // Effect: Bloom (General 20% Reduction) if (Player.Instance.CurrentTrinketStats.effectBloom) { finalCost *= 0.8f; } // Effect: Angel Eye (Silent Peek is Free) if (isSilentPeek && Player.Instance.CurrentTrinketStats.effectAngelEye) { finalCost = 0f; } // Note: Zora's Focus (effectBreeze) logic for Magic Push cost was removed as requested. // --- 2. CHECK & CONSUME --- if (selectedEffect != null && finalCost <= Player.Instance.GetCurrentFaithValue()) { Player.Instance.UpdateFaithCurrentValue(-finalCost); // Effect: Growth (Heal on Cast) if (Player.Instance.CurrentTrinketStats.effectGrowth) { int healAmt = Mathf.Max(1, (int)(Player.Instance.MaxHealth * 0.02f)); Player.Instance.ThirdPersonController.ChangeHealth(healAmt); } animationClip = selectedEffect.animClipName; // Snap Rotation if (_autoTargettingInstance != null && _autoTargettingInstance.CurrentTarget != null) { SnapLookTowardsAutoTarget(); } // Play Animation if (tpInput != null && tpInput.cc != null && tpInput.cc.animator != null) { tpInput.cc.animator.CrossFadeInFixedTime(animationClip, 0.1f); OnPlayAnimation.Invoke(); triggerOnce = true; } else { Debug.LogError("Cannot play spell animation: tpInput or its components are null."); return; } // Invoke Logic (Coroutine) selectedEffect.del?.Invoke(); // Handle Consumables (Scrolls) if (powersArea.equipSlots[equipAreaSelectedIndex].item.destroyAfterUse) { if (selectedEffect == silentPeek) { // Logic handled inside coroutine } else { powersArea.UseItem(powersArea.equipSlots[equipAreaSelectedIndex]); } } } else if (selectedEffect != null) { TryToPlayNoEnoughFaithClip(); } } // ---------------------------------------------------------------------------------- // SPELL: MAGIC PUSH // ---------------------------------------------------------------------------------- public void MagicPushAttack() { if (lastPushRoutine != null) StopCoroutine(lastPushRoutine); lastPushRoutine = StartCoroutine(MagicPushCoroutine()); } private IEnumerator MagicPushCoroutine() { EffectDesc mpush = m_effects[(int)EffectType.MAGIC_PUSH]; yield return TurnTowardTargetCoroutine(mpush.startTime); // Zora's Focus scaling logic removed from here as requested. // Magic Push is now standard size. mpush.effectObject.SetActive(false); mpush.effectObject.SetActive(true); // Apply Soulfire Damage ApplySoulfireDamage(mpush.effectObject); yield return new WaitForSeconds(mpush.delay); yield return new WaitForSeconds(mpush.endTime); mpush.effectObject.SetActive(false); yield return null; } // ---------------------------------------------------------------------------------- // SPELL: FLAME THROWER // ---------------------------------------------------------------------------------- public void FlameThrowerAttack() { StartCoroutine(FlameThrowerhCoroutine()); } private IEnumerator FlameThrowerhCoroutine() { EffectDesc flameThrowe = m_effects[(int)EffectType.FLAME_THROWER]; yield return TurnTowardTargetCoroutine(flameThrowe.startTime); flameDamager.enabled = true; ApplySoulfireDamage(flameThrowe.effectObject); flame.Play(); yield return new WaitForSeconds(flameThrowe.endTime); flame.Stop(); yield return new WaitForSeconds(flameThrowe.delay); flameDamager.enabled = false; yield return null; } // ---------------------------------------------------------------------------------- // SPELL: FIREBALL (Soulfire Damage) // ---------------------------------------------------------------------------------- public void Fireball() { StartCoroutine(FireballCoroutine()); } private IEnumerator FireballCoroutine() { EffectDesc fireballDesc = m_effects[(int)EffectType.FIREBALL]; yield return TurnTowardTargetCoroutine(fireballDesc.startTime); var fireballClone = Instantiate(fireballDesc.effectObject, fireballDesc.effectObject.transform.position, fireballDesc.effectObject.transform.rotation); fireballClone.SetActive(true); // Apply Soulfire Damage to the Clone ApplySoulfireDamage(fireballClone); RFX4_PhysicsMotion fireballMotionController = fireballClone.GetComponentInChildren(); if (fireballMotionController != null) { fireballMotionController.CollisionEnter += EnableBrieflyFireballDamager; } vObjectDamage fireballDamageComponent = fireballClone.GetComponentInChildren(); if (fireballDamageComponent != null && onHitFireball != null) { fireballDamageComponent.onHit.AddListener(onHitFireball); } AimFireball(fireballClone); Destroy(fireballClone, 10f); yield return null; } // ---------------------------------------------------------------------------------- // SPELL: SHIELD (Calmness) // ---------------------------------------------------------------------------------- public void Shield() { StopCoroutine(nameof(ShieldCoroutine)); StartCoroutine(nameof(ShieldCoroutine)); } private IEnumerator ShieldCoroutine() { shieldAnimationIsActive = true; yield return new WaitForSeconds(shield.startTime); shieldEffectIsActive = true; shieldEffectController.InitializeEffect(); shield.effectObject.SetActive(true); shieldCollisionController.shieldCollider.enabled = true; // Effect: Calmness (Shield lasts 50% longer) float finalDuration = shield.endTime; if (Player.Instance.CurrentTrinketStats.effectCalmness) { finalDuration *= 1.5f; } yield return new WaitForSeconds(finalDuration); shieldEffectController.DisableEffect(); yield return new WaitForSeconds(shield.delay / 2f); shieldEffectIsActive = false; shieldCollisionController.shieldCollider.enabled = false; yield return new WaitForSeconds(shield.delay / 2f); shield.effectObject.SetActive(false); shieldAnimationIsActive = false; } private void DisableShield() { shieldEffectIsActive = false; if (shieldCollisionController != null && shieldCollisionController.shieldCollider != null) shieldCollisionController.shieldCollider.enabled = false; shieldAnimationIsActive = false; if (shield != null && shield.effectObject != null) shield.effectObject.SetActive(false); } // ---------------------------------------------------------------------------------- // SPELL: SILENT PEEK (Angel Eye & Zora's Focus) // ---------------------------------------------------------------------------------- [Button] private void OnSilentPeek() { StopCoroutine(nameof(SilentPeekCoroutine)); StartCoroutine(nameof(SilentPeekCoroutine)); } private IEnumerator SilentPeekCoroutine() { EffectDesc peek = m_effects[(int)EffectType.SILENT_PEEK]; yield return new WaitForSeconds(peek.startTime); // Effect: Zora's Focus (effectBreeze) // User Note: "enhanced covert gaze radius and length", "keep it in not fully implemented version" // Implementation: We check the flag, but currently do not apply Radius/Length changes. if (Player.Instance.CurrentTrinketStats.effectBreeze) { // Placeholder for future logic: // float extendedDuration = peek.endTime * 1.5f; // float extendedRadius = currentRadius * 1.3f; } if (SilentPeekController.instance.IsActive()) { SilentPeekController.instance.SetActive(false); } else { if (peek.effectObject != null) { peek.effectObject.SetActive(false); peek.effectObject.SetActive(true); yield return new WaitForSeconds(peek.delay); } SilentPeekController.instance.SetActive(true, powersArea.equipSlots[equipAreaSelectedIndex].item); } yield return null; } // ---------------------------------------------------------------------------------- // SPELL: SCAN // ---------------------------------------------------------------------------------- [Button] public void Scan() { StartCoroutine(ScanCoroutine()); } private IEnumerator ScanCoroutine() { EffectDesc scan = m_effects[(int)EffectType.SCAN]; yield return new WaitForSeconds(scan.startTime); float maxRange = 50f; float speed = maxRange / (scan.endTime - scan.startTime); int mask = 1 << LayerMask.NameToLayer("Triggers") | 1 << LayerMask.NameToLayer("HiddenObject"); if (scan.effectObject) { scan.effectObject.SetActive(true); VisualEffect effect = scan.effectObject.GetComponent(); effect.Play(); } float waveEffectTimer = 0f; float waveEffectDuration = scan.endTime - scan.startTime; while (waveEffectTimer < waveEffectDuration) { Shader.SetGlobalFloat("_WaveTime", speed * waveEffectTimer); waveEffectTimer += Time.deltaTime; yield return null; } Shader.SetGlobalFloat("_WaveTime", 0f); var colliders = Physics.OverlapSphere(transform.position, maxRange, mask); foreach (var c in colliders) { var h = c.gameObject.GetComponent(); if (h != null) h.OnScanned(); } if (scan.effectObject) scan.effectObject.SetActive(false); yield return null; } // ---------------------------------------------------------------------------------- // HELPERS // ---------------------------------------------------------------------------------- // Applies Trinket Damage Multiplier (Soulfire) to any vObjectDamage found on object private void ApplySoulfireDamage(GameObject obj) { if (obj == null) return; var damageComps = obj.GetComponentsInChildren(); float mult = Player.Instance.CurrentTrinketStats.soulfireDamageMult; // Only apply if multiplier is significant if (Mathf.Abs(mult - 1f) > 0.01f) { foreach (var comp in damageComps) { comp.damage.damageValue = Mathf.RoundToInt(comp.damage.damageValue * mult); } } } public EffectDesc GetCurrentlySelectedPower() { if (powersArea == null || equipAreaSelectedIndex < 0 || equipAreaSelectedIndex >= powersArea.equipSlots.Count || powersArea.equipSlots[equipAreaSelectedIndex] == null || powersArea.equipSlots[equipAreaSelectedIndex].item == null) { currentSpellFaithCost = int.MaxValue; currentSelectedSpellName = ""; return null; } bItem selectedSpellItem = powersArea.equipSlots[equipAreaSelectedIndex].item; currentSelectedSpellName = selectedSpellItem.name; currentSpellFaithCost = selectedSpellItem.GetItemAttribute(bItemAttributes.Faith).value; return m_effects.Find(effect => effect.name == selectedSpellItem.name || effect.secondaryName == selectedSpellItem.name); } public void SelectPowerBasedOnArea(int newIndex) { equipAreaSelectedIndex = newIndex; selectedEffect = GetCurrentlySelectedPower(); } private void TryToPlayCantDoThatYetClip() { if (!canPlayCantDoClip) return; canPlayCantDoClip = false; DOVirtual.DelayedCall(1f, () => canPlayCantDoClip = true); var text = "Spell is already active"; if (bItemCollectionDisplay.Instance != null) bItemCollectionDisplay.Instance.FadeText(text, 4, 0.25f); if (Player.Instance != null) Player.Instance.PlayICantDoThatYet(); } private void TryToPlayNoEnoughFaithClip() { if (!canPlayNoFaithClip) return; canPlayNoFaithClip = false; DOVirtual.DelayedCall(1.5f, () => canPlayNoFaithClip = true); var text = "Not enough Faith"; if (bItemCollectionDisplay.Instance != null) bItemCollectionDisplay.Instance.FadeText(text, 4, 0.25f); if (Player.Instance != null) Player.Instance.PlayNoFaithClip(); } protected virtual void AnimationBehaviour() { if (tpInput == null || tpInput.cc == null || tpInput.cc.animator == null || string.IsNullOrEmpty(animationClip)) { isPlaying = false; return; } isPlaying = tpInput.cc.animator.GetCurrentAnimatorStateInfo(spellLayerIndex).IsName(animationClip) || tpInput.cc.animator.GetNextAnimatorStateInfo(spellLayerIndex).IsName(animationClip); if (isPlaying) { if (tpInput.cc.animator.GetCurrentAnimatorStateInfo(spellLayerIndex).IsName(animationClip) && tpInput.cc.animator.GetCurrentAnimatorStateInfo(spellLayerIndex).normalizedTime >= animationEnd) { if (triggerOnce) { triggerOnce = false; OnEndAnimation.Invoke(); } } } else { if (triggerOnce) triggerOnce = false; } } private IEnumerator TurnTowardTargetCoroutine(float maxDuration) { if (_autoTargettingInstance == null || _autoTargettingInstance.CurrentTarget == null) { if (maxDuration > 0) yield return new WaitForSeconds(maxDuration); yield break; } float timeElapsed = 0; while (timeElapsed < maxDuration) { if (_autoTargettingInstance.CurrentTarget != null) { vFSMBehaviourController currentTarget = _autoTargettingInstance.CurrentTarget; Transform playerTransform = transform; float distSqr = (currentTarget.transform.position - playerTransform.position).sqrMagnitude; if (distSqr <= _autoTargettingInstance.maxTargetingDistance * _autoTargettingInstance.maxTargetingDistance) { if (_autoTargettingInstance.IsTargetInAngle(playerTransform, currentTarget, _autoTargettingInstance.targetingAngleThreshold)) { Vector3 directionToTarget = currentTarget.transform.position - playerTransform.position; directionToTarget.y = 0f; if (directionToTarget.sqrMagnitude > 0.0001f) { Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized); playerTransform.rotation = Quaternion.RotateTowards(playerTransform.rotation, targetRotation, Time.deltaTime * _autoTargettingInstance.playerRotationSpeed); } } } } else { yield break; } timeElapsed += Time.deltaTime; yield return null; } } private void SnapLookTowardsAutoTarget() { if (_autoTargettingInstance == null || _autoTargettingInstance.CurrentTarget == null) return; vFSMBehaviourController currentTarget = _autoTargettingInstance.CurrentTarget; Transform playerTransform = transform; float distSqr = (currentTarget.transform.position - playerTransform.position).sqrMagnitude; if (distSqr > _autoTargettingInstance.maxTargetingDistance * _autoTargettingInstance.maxTargetingDistance) return; if (!_autoTargettingInstance.IsTargetInAngle(playerTransform, currentTarget, _autoTargettingInstance.targetingAngleThreshold)) return; Vector3 directionToTarget = currentTarget.transform.position - playerTransform.position; directionToTarget.y = 0f; if (directionToTarget.sqrMagnitude > 0.0001f) { playerTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized); } } private void AimFireball(GameObject fireballClone) { Vector3 aimDirection = transform.forward; if (_autoTargettingInstance != null && _autoTargettingInstance.CurrentTarget != null) { vFSMBehaviourController autoTarget = _autoTargettingInstance.CurrentTarget; Transform playerTransform = transform; float distSqrToAutoTarget = (autoTarget.transform.position - playerTransform.position).sqrMagnitude; if (distSqrToAutoTarget <= _autoTargettingInstance.maxTargetingDistance * _autoTargettingInstance.maxTargetingDistance && _autoTargettingInstance.IsTargetInAngle(playerTransform, autoTarget, _autoTargettingInstance.targetingAngleThreshold)) { Vector3 targetPosition = autoTarget.transform.position; targetPosition.y += fireballTargetYPositionOffset; aimDirection = (targetPosition - fireballClone.transform.position).normalized; } } if (aimDirection == transform.forward && lockOn != null && lockOn.isLockingOn && lockOn.currentTarget != null) { Vector3 targetPosition = lockOn.currentTarget.position; targetPosition.y += fireballTargetYPositionOffset; aimDirection = (targetPosition - fireballClone.transform.position).normalized; } else if (aimDirection == transform.forward && lockOn != null) { List closeEnemies = lockOn.GetNearbyTargets(); if (closeEnemies.Count > 0) { Transform bestFallbackTarget = null; float minAngle = float.MaxValue; foreach (var enemyTransform in closeEnemies) { Vector3 directionToEnemyFromPlayer = (enemyTransform.position - transform.position).normalized; float angleToEnemy = Vector3.Angle(transform.forward, directionToEnemyFromPlayer); if (Vector3.Dot(transform.forward, directionToEnemyFromPlayer) > fireballAimerThreshold) { if (angleToEnemy < minAngle) { minAngle = angleToEnemy; bestFallbackTarget = enemyTransform; } } } if (bestFallbackTarget != null) { Vector3 targetPosition = bestFallbackTarget.position; targetPosition.y += fireballTargetYPositionOffset; aimDirection = (targetPosition - fireballClone.transform.position).normalized; } } } Vector3 finalAimDirection = new Vector3(aimDirection.x, aimDirection.y + fireballAimerHeightAdjuster, aimDirection.z); if (finalAimDirection.sqrMagnitude > 0.001f) { fireballClone.transform.rotation = Quaternion.LookRotation(finalAimDirection.normalized); } } private void EnableBrieflyFireballDamager(object sender, RFX4_PhysicsMotion.RFX4_CollisionInfo e) { RFX4_PhysicsMotion rFX4_PhysicsMotion = (RFX4_PhysicsMotion)sender; CapsuleCollider collider = rFX4_PhysicsMotion.GetComponentInChildren(); if (collider != null) StartCoroutine(EnableBrieflyFireballDamagerCoroutine(collider)); } private IEnumerator EnableBrieflyFireballDamagerCoroutine(CapsuleCollider collider) { collider.enabled = true; yield return new WaitForSeconds(fireballDamagerDuration); collider.enabled = false; } } }