using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Invector; using Invector.vCharacterController; using Invector.vCharacterController.vActions; using Sirenix.OdinInspector; // using UnityEditor; using UnityEngine; using UnityEngine.Events; using UnityEngine.VFX; // using static Invector.vObjectDamage; 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 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; // REMOVED: public bool enableAutoTargetIntegration = true; // REMOVED: public float maxTurnTowardDistance = 10f; // REMOVED: public float rotationSpeed = 500f; // REMOVED: public float degreeThreshold = 100f; (already removed in previous step) public UnityAction onHitFireball; private void Awake() { tpInput = GetComponent(); if (Player.Instance != null) { _autoTargettingInstance = Player.Instance.AutoTarget; if (_autoTargettingInstance == null) // Simplified warning { Debug.LogWarning("MagicAttacks: AutoTargetting component not found on Player.Instance.AutoTarget. Auto-targeting features will not be available."); } } else { Debug.LogError("MagicAttacks: Player.Instance is null in Awake. Cannot get AutoTargetting component."); } lockOn = GetComponent(); 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]; shieldEffectController = shield.effectObject.GetComponent(); shieldCollisionController = shield.effectObject.GetComponentInChildren(); 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(); } } 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; if (shield.effectObject) { shield.effectObject.SetActive(false); } flame = flameThrowe.effectObject.GetComponent(); flameDamager = flameThrowe.effectObject.GetComponentInChildren(); silentPeek.del = OnSilentPeek; } private void OnDisable() { if (shieldAnimationIsActive) { DisableShield(); } } private void OnEnable() { canPlayNoFaithClip = true; canPlayCantDoClip = true; } [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); 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; } 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); mpush.effectObject.SetActive(false); mpush.effectObject.SetActive(true); yield return new WaitForSeconds(mpush.delay); Debug.Log("Bum!"); yield return new WaitForSeconds(mpush.endTime); mpush.effectObject.SetActive(false); yield return null; } public void FlameThrowerAttack() { StartCoroutine(FlameThrowerhCoroutine()); } private IEnumerator FlameThrowerhCoroutine() { EffectDesc flameThrowe = m_effects[(int)EffectType.FLAME_THROWER]; yield return TurnTowardTargetCoroutine(flameThrowe.startTime); flameDamager.enabled = true; flame.Play(); yield return new WaitForSeconds(flameThrowe.endTime); flame.Stop(); yield return new WaitForSeconds(flameThrowe.delay); flameDamager.enabled = false; yield return null; } [Button] public void Scan() { StartCoroutine(ScanCoroutine()); } private IEnumerator ScanCoroutine() { EffectDesc scan = m_effects[(int)EffectType.SCAN]; yield return new WaitForSeconds(scan.startTime); float time = scan.startTime - scan.delay; 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; } public void Fireball() { StartCoroutine(FireballCoroutine()); } private IEnumerator TurnTowardTargetCoroutine(float maxDuration) { // *** MODIFIED: Condition relies only on _autoTargettingInstance and its CurrentTarget *** if (_autoTargettingInstance == null || _autoTargettingInstance.CurrentTarget == null) { if (maxDuration > 0) yield return new WaitForSeconds(maxDuration); yield break; } float timeElapsed = 0; while (timeElapsed < maxDuration) { // Re-check CurrentTarget in loop in case it becomes null (e.g., target dies mid-turn) 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); // *** MODIFIED: Use playerRotationSpeed from AutoTargetting *** playerTransform.rotation = Quaternion.RotateTowards(playerTransform.rotation, targetRotation, Time.deltaTime * _autoTargettingInstance.playerRotationSpeed); } } } } else // Target became null during the loop { yield break; // Exit if no target } timeElapsed += Time.deltaTime; yield return null; } } private IEnumerator FireballCoroutine() { EffectDesc fireballDesc = m_effects[(int)EffectType.FIREBALL]; // Renamed to avoid conflict yield return TurnTowardTargetCoroutine(fireballDesc.startTime); var fireballClone = Instantiate(fireballDesc.effectObject, fireballDesc.effectObject.transform.position, fireballDesc.effectObject.transform.rotation); fireballClone.SetActive(true); 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; } 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 void AimFireball(GameObject fireballClone) { Vector3 aimDirection = transform.forward; // *** MODIFIED: Condition relies only on _autoTargettingInstance and its CurrentTarget *** 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 IEnumerator EnableBrieflyFireballDamagerCoroutine(CapsuleCollider collider) { collider.enabled = true; yield return new WaitForSeconds(fireballDamagerDuration); collider.enabled = false; } 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; yield return new WaitForSeconds(shield.endTime); 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); } [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; public bool isPlaying; protected bool triggerOnce; protected bThirdPersonInput tpInput; internal bool shieldEffectIsActive; private bool shieldAnimationIsActive; private int equipAreaSelectedIndex = 0; 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(); } 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; } if (selectedEffect != null && currentSpellFaithCost <= Player.Instance.GetCurrentFaithValue()) { Player.Instance.UpdateFaithCurrentValue(-currentSpellFaithCost); animationClip = selectedEffect.animClipName; // *** MODIFIED: Snap look if AutoTargetting is available and has a target *** if (_autoTargettingInstance != null && _autoTargettingInstance.CurrentTarget != null) { SnapLookTowardsAutoTarget(); } 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; } selectedEffect.del?.Invoke(); if (powersArea.equipSlots[equipAreaSelectedIndex].item.destroyAfterUse) { if (selectedEffect == silentPeek) { // Special handling } else { powersArea.UseItem(powersArea.equipSlots[equipAreaSelectedIndex]); } } } else if (selectedEffect != null) { TryToPlayNoEnoughFaithClip(); } } private void SnapLookTowardsAutoTarget() { // *** MODIFIED: Condition relies only on _autoTargettingInstance and its CurrentTarget *** // This check is somewhat redundant due to call site, but good for safety if called elsewhere. 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 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(); } 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(); } 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; } } } } }