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; // Best to remove if not strictly needed for runtime using UnityEngine; using UnityEngine.Events; using UnityEngine.VFX; // using static Invector.vObjectDamage; // Not used directly, consider removing using DG.Tweening; using Invector.vCharacterController.AI.FSMBehaviour; using Beyond; // For Player, GameStateManager, AutoTargetting (if Player.Instance.AutoTarget is of this type) 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; // Retained for fallback aiming or non-combat interactions if needed private const float fireballAimerThreshold = -1.0f; // Used for fallback aiming if no auto-target 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; // NEW: AutoTargetting fields private AutoTargetting _autoTargettingInstance; [BoxGroup("Auto targetting")] [Tooltip("Enable to use AutoTargetting for player rotation and spell aiming.")] public bool enableAutoTargetIntegration = true; [BoxGroup("Auto targetting")] [Tooltip("Max distance for player to turn towards an auto-target during spell casting.")] public float maxTurnTowardDistance = 10f; [BoxGroup("Auto targetting")] [Tooltip("Rotation speed when turning towards an auto-target.")] public float rotationSpeed = 500f; [BoxGroup("Auto targetting")] [Tooltip("Angle threshold within which the player will rotate towards an auto-target.")] public float degreeThreshold = 100f; public UnityAction onHitFireball; private void Awake() { tpInput = GetComponent(); // NEW: Initialize AutoTargetting instance if (Player.Instance != null) { _autoTargettingInstance = Player.Instance.AutoTarget; // Assuming Player.Instance has an AutoTarget property of type AutoTargetting if (_autoTargettingInstance == null && enableAutoTargetIntegration) { Debug.LogWarning("MagicAttacks: AutoTargetting component not found on Player.Instance.AutoTarget, but enableAutoTargetIntegration is true. Targeting features will be limited."); } } else { Debug.LogError("MagicAttacks: Player.Instance is null in Awake. Cannot get AutoTargetting component."); } lockOn = GetComponent(); // Keep for potential fallback 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(SilentPeekCoroutine()); StartCoroutine(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]; // MODIFIED: Use TurnTowardTargetCoroutine for potential rotation during wind-up yield return TurnTowardTargetCoroutine(mpush.startTime); mpush.effectObject.SetActive(false); mpush.effectObject.SetActive(true); yield return new WaitForSeconds(mpush.delay); Debug.Log("Bum!"); // Consider replacing with actual effect logic yield return new WaitForSeconds(mpush.endTime); // This is endTime after delay, might be confusing. mpush.effectObject.SetActive(false); yield return null; } public void FlameThrowerAttack() { StartCoroutine(FlameThrowerhCoroutine()); } private IEnumerator FlameThrowerhCoroutine() { EffectDesc flameThrowe = m_effects[(int)EffectType.FLAME_THROWER]; // MODIFIED: TurnTowardTargetCoroutine will handle rotation based on AutoTargetting yield return TurnTowardTargetCoroutine(flameThrowe.startTime); flameDamager.enabled = true; flame.Play(); yield return new WaitForSeconds(flameThrowe.endTime); // Duration of flame flame.Stop(); yield return new WaitForSeconds(flameThrowe.delay); // Cooldown/after-effect flameDamager.enabled = false; yield return null; } [Button] public void Scan() { StartCoroutine(ScanCoroutine()); } private IEnumerator ScanCoroutine() { EffectDesc scan = m_effects[(int)EffectType.SCAN]; // Scan might not need rotation, but if it had a wind-up animation, TurnTowardTargetCoroutine could be used. yield return new WaitForSeconds(scan.startTime); float time = scan.startTime - scan.delay; // This calculation seems off if delay is for after effect. Assuming startTime is actual start. float maxRange = 50f; float speed = maxRange / (scan.endTime - scan.startTime); // scan.endTime is duration here 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); // Use timer relative to effect start waveEffectTimer += Time.deltaTime; yield return null; } Shader.SetGlobalFloat("_WaveTime", 0f); // Reset shader global 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); // Deactivate after use yield return null; } public void Fireball() { StartCoroutine(FireballCoroutine()); } // MODIFIED: Centralized coroutine for turning towards target during spell animations private IEnumerator TurnTowardTargetCoroutine(float maxDuration) { if (!enableAutoTargetIntegration || _autoTargettingInstance == null) { // If auto-targeting is off or unavailable, just wait for the duration without rotation. 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; // Character's transform float distSqr = (currentTarget.transform.position - playerTransform.position).sqrMagnitude; // Check distance using MagicAttacks.maxTurnTowardDistance if (distSqr <= maxTurnTowardDistance * maxTurnTowardDistance) { // Check angle using MagicAttacks.degreeThreshold and AutoTargetting's utility if (_autoTargettingInstance.IsTargetInAngle(playerTransform, currentTarget, degreeThreshold)) { Vector3 directionToTarget = currentTarget.transform.position - playerTransform.position; directionToTarget.y = 0f; // Horizontal rotation only if (directionToTarget.sqrMagnitude > 0.0001f) // Ensure there's a direction { Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized); // Use MagicAttacks.rotationSpeed for the rotation playerTransform.rotation = Quaternion.RotateTowards(playerTransform.rotation, targetRotation, Time.deltaTime * rotationSpeed); } } } } timeElapsed += Time.deltaTime; yield return null; } } private IEnumerator FireballCoroutine() { EffectDesc fireball = m_effects[(int)EffectType.FIREBALL]; // MODIFIED: Use new TurnTowardTargetCoroutine yield return TurnTowardTargetCoroutine(fireball.startTime); var fireballClone = Instantiate(fireball.effectObject, fireball.effectObject.transform.position, fireball.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); } // MODIFIED: Use new AimFireball method AimFireball(fireballClone); Destroy(fireballClone, 10f); // Self-destruct after time 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(); // Assuming damager is a CapsuleCollider if(collider != null) StartCoroutine(EnableBrieflyFireballDamagerCoroutine(collider)); } // NEW: Refactored fireball aiming logic private void AimFireball(GameObject fireballClone) { Vector3 aimDirection = transform.forward; // Default aim is player's forward if (enableAutoTargetIntegration && _autoTargettingInstance != null && _autoTargettingInstance.CurrentTarget != null) { vFSMBehaviourController autoTarget = _autoTargettingInstance.CurrentTarget; Vector3 targetPosition = autoTarget.transform.position; targetPosition.y += fireballTargetYPositionOffset; // Adjust height for aiming aimDirection = (targetPosition - fireballClone.transform.position).normalized; } else if (lockOn != null && lockOn.isLockingOn && lockOn.currentTarget != null) // Fallback to bLockOn target { Vector3 targetPosition = lockOn.currentTarget.position; targetPosition.y += fireballTargetYPositionOffset; aimDirection = (targetPosition - fireballClone.transform.position).normalized; } else if (lockOn != null) // Fallback to nearest enemy in front (from bLockOn) { List closeEnemies = lockOn.GetNearbyTargets(); if (closeEnemies.Count > 0) { foreach (var enemyTransform in closeEnemies) // Find first suitable enemy in front { Vector3 targetPosition = enemyTransform.position; targetPosition.y += fireballTargetYPositionOffset; Vector3 directionToEnemy = (targetPosition - fireballClone.transform.position).normalized; // Check if enemy is generally in front of the player (fireball origin) if (Vector3.Dot(transform.forward, directionToEnemy) > fireballAimerThreshold) { aimDirection = directionToEnemy; break; } } } } // Apply calculated aim direction to the fireball, adding vertical adjustment Vector3 finalAimDirection = new Vector3(aimDirection.x, aimDirection.y + fireballAimerHeightAdjuster, aimDirection.z); if (finalAimDirection.sqrMagnitude > 0.001f) { fireballClone.transform.rotation = Quaternion.LookRotation(finalAimDirection.normalized); } // If aimDirection is zero (shouldn't happen with defaults), it will use its instantiated rotation. } private IEnumerator EnableBrieflyFireballDamagerCoroutine(CapsuleCollider collider) { collider.enabled = true; yield return new WaitForSeconds(fireballDamagerDuration); collider.enabled = false; } public void Shield() { StopCoroutine(ShieldCoroutine()); // Ensure only one shield coroutine runs StartCoroutine(ShieldCoroutine()); } private IEnumerator ShieldCoroutine() { shieldAnimationIsActive = true; // Shield typically doesn't need offensive targeting/rotation. // If there was a wind-up animation where player *should* face an enemy, TurnTowardTargetCoroutine could be used. yield return new WaitForSeconds(shield.startTime); shieldEffectIsActive = true; shieldEffectController.InitializeEffect(); shield.effectObject.SetActive(true); shieldCollisionController.shieldCollider.enabled = true; yield return new WaitForSeconds(shield.endTime); // Duration shield is active shieldEffectController.DisableEffect(); yield return new WaitForSeconds(shield.delay / 2f); // Fade out time part 1 shieldEffectIsActive = false; shieldCollisionController.shieldCollider.enabled = false; yield return new WaitForSeconds(shield.delay / 2f); // Fade out time part 2 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; // This will be set by selectedEffect.animClipName [Tooltip("Where in the end of the animation will trigger the event OnEndAnimation")] public float animationEnd = 0.8f; public UnityEvent OnPlayAnimation; // Consider if this is still needed or how it fits public UnityEvent OnEndAnimation; public bool isPlaying; // Tracks if the spell animation is currently playing protected bool triggerOnce; // For OnEndAnimation event protected vThirdPersonInput tpInput; // Renamed from tpInput to avoid conflict with Invector's tpInput if any confusion internal bool shieldEffectIsActive; private bool shieldAnimationIsActive; private int equipAreaSelectedIndex = 0; protected virtual void LateUpdate() // LateUpdate for animation state checks is common { 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(); // Ensure current selected effect is fetched if (selectedEffect == shield && shieldAnimationIsActive) { TryToPlayCantDoThatYetClip(); return; } if (Player.Instance == null) // Safeguard { Debug.LogError("Player.Instance is null. Cannot cast spell."); return; } if (selectedEffect != null && currentSpellFaithCost <= Player.Instance.GetCurrentFaithValue()) { Player.Instance.UpdateFaithCurrentValue(-currentSpellFaithCost); animationClip = selectedEffect.animClipName; // Set animation clip for AnimationBehaviour // NEW: Perform initial snap rotation if auto-targeting is enabled if (enableAutoTargetIntegration) { SnapLookTowardsAutoTarget(); } // else // OLD logic for turning - can be removed or kept as fallback // { // TryToTurnTowordsEnemy(); // } if (tpInput != null && tpInput.cc != null && tpInput.cc.animator != null) { tpInput.cc.animator.CrossFadeInFixedTime(animationClip, 0.1f); OnPlayAnimation.Invoke(); // Invoke OnPlay event triggerOnce = true; // Allow OnEndAnimation to be called } else { Debug.LogError("Cannot play spell animation: tpInput or its components are null."); return; // Don't proceed to call delegate if animation components are missing } selectedEffect.del?.Invoke(); // Call the spell's primary action delegate if (powersArea.equipSlots[equipAreaSelectedIndex].item.destroyAfterUse) { if (selectedEffect == silentPeek) { // Special handling for Silent Peek item destruction (likely in its controller) } else { powersArea.UseItem(powersArea.equipSlots[equipAreaSelectedIndex]); } } } else if (selectedEffect != null) // Not enough faith { TryToPlayNoEnoughFaithClip(); } // If selectedEffect is null, nothing happens (no spell selected/valid) } // NEW: Method for an immediate snap-look towards the auto-target private void SnapLookTowardsAutoTarget() { if (_autoTargettingInstance == null || _autoTargettingInstance.CurrentTarget == null) { return; // No auto-target system or no current target } vFSMBehaviourController currentTarget = _autoTargettingInstance.CurrentTarget; Transform playerTransform = transform; // Check distance condition from MagicAttacks settings float distSqr = (currentTarget.transform.position - playerTransform.position).sqrMagnitude; if (distSqr > maxTurnTowardDistance * maxTurnTowardDistance) { return; // Target is too far for this specific snap-look interaction } // Check angle condition from MagicAttacks settings if (!_autoTargettingInstance.IsTargetInAngle(playerTransform, currentTarget, degreeThreshold)) { return; // Target is not within the desired cone for snap-look } Vector3 directionToTarget = currentTarget.transform.position - playerTransform.position; directionToTarget.y = 0f; // Horizontal rotation only if (directionToTarget.sqrMagnitude > 0.0001f) { playerTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized); } } private void TryToPlayCantDoThatYetClip() { if (!canPlayCantDoClip) return; canPlayCantDoClip = false; DOVirtual.DelayedCall(1f, () => canPlayCantDoClip = true); // Reset flag var text = "Spell is already active"; // Example message if (bItemCollectionDisplay.Instance != null) bItemCollectionDisplay.Instance.FadeText(text, 4, 0.25f); if (Player.Instance != null) Player.Instance.PlayICantDoThatYet(); // Assuming this method exists on Player } private void TryToPlayNoEnoughFaithClip() { if (!canPlayNoFaithClip) return; canPlayNoFaithClip = false; DOVirtual.DelayedCall(1.5f, () => canPlayNoFaithClip = true); // Reset flag var text = "Not enough Faith"; if (bItemCollectionDisplay.Instance != null) bItemCollectionDisplay.Instance.FadeText(text, 4, 0.25f); if (Player.Instance != null) Player.Instance.PlayNoFaithClip(); // Assuming this method exists on Player } 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; // Store for display or debugging 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(); // Update current effect based on selection } // OLD rotation methods - can be removed or commented out if new system is preferred /* private void LerpRotation() { // ... original LerpRotation code using GetNearestEnemy ... } private bool IsEnemyInAngleRange(vFSMBehaviourController ai) { // ... original IsEnemyInAngleRange code ... } private vFSMBehaviourController GetNearestEnemy(ref float minDist) { // ... original GetNearestEnemy code ... } private void TryToTurnTowordsEnemy() // Replaced by SnapLookTowardsAutoTarget { // ... original TryToTurnTowordsEnemy code ... } */ protected virtual void AnimationBehaviour() { if (tpInput == null || tpInput.cc == null || tpInput.cc.animator == null || string.IsNullOrEmpty(animationClip)) { isPlaying = false; return; } // isPlaying should reflect if the *specific spell animation* is active. 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 not playing the specific animation clip, ensure triggerOnce is reset // This handles cases where animation might be interrupted before reaching animationEnd if (triggerOnce) { triggerOnce = false; // Optionally, invoke OnEndAnimation if it should always fire on exiting the state, // but current logic only fires it if normalizedTime >= animationEnd. } } } } }