using System; using Beyond; // For Player (ensure Player.cs is in this namespace or adjust) using Invector.vCharacterController.AI.FSMBehaviour; // For vFSMBehaviourController using UnityEngine; using System.Collections.Generic; // For List namespace Invector.vMelee { using vEventSystems; // For vIAttackListener public class vMeleeAttackControl : StateMachineBehaviour { [Header("Damage Window")] [Tooltip("NormalizedTime of Active Damage")] public float startDamage = 0.05f; [Tooltip("NormalizedTime of Disable Damage")] public float endDamage = 0.9f; [Header("Attack Properties")] public int damageMultiplier = 1; public int recoilID; public int reactionID; public vAttackType meleeAttackType = vAttackType.Unarmed; [Tooltip("You can use a name as reference to trigger a custom HitDamageParticle")] public string damageType; [Tooltip("Body parts to activate for damage detection.")] public List bodyParts = new List { "RightLowerArm" }; // Default, adjust as needed [Header("Hit Effects")] public bool ignoreDefense; public bool activeRagdoll; [vHideInInspector("activeRagdoll")] // Assuming vHideInInspector is a valid attribute in your project [Tooltip("Time to keep Ragdoll active if activeRagdoll is true.")] public float senselessTime = 2f; [Header("Attack Flow")] [Tooltip("Check true in the last attack of your combo to reset the FSM attack triggers.")] public bool resetAttackTrigger; [Tooltip("Normalized time point to reset attack triggers if resetAttackTrigger is true.")] public float resetTriggerBeforeTime = 0.8f; // Example: reset triggers before animation fully ends [Header("Slow Motion Settings")] [Tooltip("Enable slow motion effect during this attack based on conditions below.")] public bool useAttackTimeScale = false; [Tooltip("Distance within which the current auto-target must be for slow motion to consider activating.")] public float slowMoActivationDistance = 3f; [Tooltip("Target health threshold below which slow motion might activate (if near).")] public float lowHealthTh = 10f; [Tooltip("Time scale to apply during slow motion.")] public float attackTimeScale = 0.2f; [Tooltip("Normalized time to start the slow motion window. If < 0, uses 'startDamage'.")] public float attackTimeScaleStart = -1f; [Tooltip("Normalized time to end the slow motion window. If < 0, uses 'endDamage'.")] public float attackTimeScaleEnd = -1f; [Header("Rotation Settings")] [Tooltip("If true, the character will attempt to rotate towards the auto-target during this attack state.")] public bool rotatePlayerTowardsTarget; [Tooltip("The angle (in degrees from player's forward) within which the auto-target must be for rotation to engage.")] public float rotationActivationAngle = 45f; [Header("Debug")] public bool debug = false; // Private state variables private bool isActive; // Is damage window active private vIAttackListener mFighter; private bool isAttacking; // Is this attack state logic considered "attacking" (for trigger resets, OnDisable) private bool m_hasScaledTime; // Has slow motion been activated in this instance of the state private AutoTargetting _autoTargettingInstance; override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { mFighter = animator.GetComponent(); if (Player.Instance != null) { _autoTargettingInstance = Player.Instance.AutoTarget; } if (_autoTargettingInstance == null && debug) { Debug.LogWarning($"({damageType}) AutoTargetting instance not found via Player.Instance.AutoTarget on {animator.name}."); } isAttacking = true; // Mark as actively in an attack process isActive = false; // Damage window is not active yet m_hasScaledTime = false; // Slow motion not yet activated for this state instance if (mFighter != null) { mFighter.OnEnableAttack(); // Notify listener attack is starting } if (debug) Debug.Log($"({damageType}) OnStateEnter: {animator.name}, Layer: {layerIndex}"); // Initialize slow-mo start/end times if they are set to use damage window timings if (attackTimeScaleStart < 0f) attackTimeScaleStart = startDamage; if (attackTimeScaleEnd < 0f) attackTimeScaleEnd = endDamage; } override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // Normalized time within the current loop of the animation (0 to 1) float currentNormalizedTime = stateInfo.normalizedTime % 1; if (currentNormalizedTime == 0 && stateInfo.normalizedTime > 0.5f) currentNormalizedTime = 1f; // Handle end of non-looping anim // --- ROTATION LOGIC --- if (rotatePlayerTowardsTarget && _autoTargettingInstance != null && _autoTargettingInstance.CurrentTarget != null) { // animator.transform is the player's transform if (_autoTargettingInstance.IsTargetInAngle(animator.transform, _autoTargettingInstance.CurrentTarget, rotationActivationAngle)) { _autoTargettingInstance.ExecuteRotationTowardsCurrentTarget(Time.deltaTime); } } // --- SLOW MOTION LOGIC --- if (useAttackTimeScale) // Only process if slow-mo is enabled for this attack { UpdateSlowMotion(animator, stateInfo, currentNormalizedTime); } // --- DAMAGE WINDOW LOGIC --- if (!isActive && currentNormalizedTime >= startDamage && currentNormalizedTime <= endDamage) { if (debug) Debug.Log($"({damageType}) Enable Damage: normTime={currentNormalizedTime:F2} (Start:{startDamage:F2}, End:{endDamage:F2})"); isActive = true; ActiveDamage(animator, true); } else if (isActive && currentNormalizedTime > endDamage) { if (debug) Debug.Log($"({damageType}) Disable Damage: normTime={currentNormalizedTime:F2} > {endDamage:F2}"); isActive = false; ActiveDamage(animator, false); } // --- ATTACK STATE AND TRIGGER RESET LOGIC --- // This section handles logic that should happen once conditions are met within the active attack. if (isAttacking) // Only process these if we are still in the "attacking process" phase of this state { // Condition to call OnDisableAttack (e.g., after damage window and before full exit) if (currentNormalizedTime > endDamage) { if (mFighter != null) mFighter.OnDisableAttack(); isAttacking = false; // Mark that OnDisableAttack has been called for this state instance if (debug) Debug.Log($"({damageType}) OnDisableAttack called: normTime={currentNormalizedTime:F2}"); } // Condition to reset FSM attack triggers earlier in the animation if specified else if (resetAttackTrigger && currentNormalizedTime >= resetTriggerBeforeTime) { if (mFighter != null) mFighter.ResetAttackTriggers(); // To prevent multiple calls if animation loops or stays in this normalized time range: // One option is to rely on mFighter.ResetAttackTriggers being idempotent. // Another is to set a flag, but since `isAttacking` might be turned false by OnDisableAttack, // we might need a separate flag or ensure resetTriggerBeforeTime < endDamage if OnDisableAttack also resets. // For simplicity, current Invector often handles ResetAttackTriggers robustly if called multiple times. if (debug) Debug.Log($"({damageType}) ResetAttackTriggers called: normTime={currentNormalizedTime:F2}"); } } } override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (debug) Debug.Log($"({damageType}) OnStateExit: {animator.name}"); // Ensure damage is disabled if state exits while damage window was active if (isActive) { isActive = false; ActiveDamage(animator, false); if (debug) Debug.Log($"({damageType}) Damage disabled on StateExit."); } // Ensure OnDisableAttack is called if it hasn't been already (e.g., if state was exited prematurely) if (isAttacking && mFighter != null) { mFighter.OnDisableAttack(); if (debug) Debug.Log($"({damageType}) OnDisableAttack called on StateExit (fallback)."); } isAttacking = false; // Reset for next entry // Reset slow motion flag m_hasScaledTime = false; // Invector's vMeleeManager or vIAttackListener typically handles final trigger resets on its own // when an attack sequence ends or on certain conditions. Explicitly calling here might be redundant // or interfere if `resetAttackTrigger` is meant for mid-combo resets via `resetTriggerBeforeTime`. // If `resetAttackTrigger` specifically means "reset on exit of this state", then: // if (mFighter != null && resetAttackTrigger) { // mFighter.ResetAttackTriggers(); // } } private void UpdateSlowMotion(Animator animator, AnimatorStateInfo stateInfo, float currentNormalizedTime) { // This method is called only if useAttackTimeScale is true for the component. if (_autoTargettingInstance == null) return; // No system to get target info from. if (!m_hasScaledTime) // If slow motion hasn't been activated yet in this state instance { // Check if we are within the normalized time window for slow motion if (currentNormalizedTime >= attackTimeScaleStart && currentNormalizedTime <= attackTimeScaleEnd) { bool triggerSlowMo = false; if (_autoTargettingInstance.CurrentTarget != null) { // Check distance to CurrentTarget float distSqr = (_autoTargettingInstance.CurrentTarget.transform.position - animator.transform.position).sqrMagnitude; bool targetNear = distSqr <= slowMoActivationDistance * slowMoActivationDistance; if (targetNear) { // If always use slow-mo when near, or if target health is low float currentTargetHealth = _autoTargettingInstance.GetCurrentTargetHealth(); bool targetHealthLow = currentTargetHealth > 0f && currentTargetHealth < lowHealthTh; // The original logic was: TargetNear() && (useAttackTimeScale || NearHealthLow()) // Since this whole UpdateSlowMotion is wrapped in `if (useAttackTimeScale)`, // it means `useAttackTimeScale` (the bool field) is the main switch. // The condition effectively becomes: if targetNear AND (this attack ALWAYS uses slowmo OR target health is low) // The bool `useAttackTimeScale` on the component acts as the "always use slowmo if near" part. if (targetHealthLow) // If health is low, it's an additional trigger. { triggerSlowMo = true; } else if(this.useAttackTimeScale) // if useAttackTimeScale is true, it means activate if near, regardless of health. { triggerSlowMo = true; } } } else { // No current target, so no target-dependent slow motion. // If `useAttackTimeScale` implies slow-mo even without a target (e.g. for stylistic effect), that logic would go here. // Based on parameters, it seems target-dependent. } if (triggerSlowMo) { if (TimeController.Instance != null) { float slowMoEffectDuration = (attackTimeScaleEnd - currentNormalizedTime) * stateInfo.length; if (slowMoEffectDuration > 0.01f) // Ensure a meaningful duration { TimeController.Instance.SetTimeScaleForSec(attackTimeScale, slowMoEffectDuration); if (debug) Debug.Log($"({damageType}) Slow-mo ACTIVATED. Target: {_autoTargettingInstance.CurrentTarget?.name ?? "N/A"}. Duration: {slowMoEffectDuration:F2}s. NormTime: {currentNormalizedTime:F2}"); } else if (debug) Debug.Log($"({damageType}) Slow-mo trigger met, but calculated duration too short ({slowMoEffectDuration:F2}s). NormTime: {currentNormalizedTime:F2}"); } m_hasScaledTime = true; } } } else if (currentNormalizedTime > attackTimeScaleEnd && m_hasScaledTime) // If slow-mo was active and window has passed { // TimeController.Instance is responsible for restoring time scale after its set duration. // We just reset our flag for this state instance. m_hasScaledTime = false; if (debug) Debug.Log($"({damageType}) Slow-mo window ended (normTime={currentNormalizedTime:F2}). m_hasScaledTime reset."); } } void ActiveDamage(Animator animator, bool value) { var meleeManager = animator.GetComponent(); // Invector's manager if (meleeManager) { meleeManager.SetActiveAttack(bodyParts, meleeAttackType, value, damageMultiplier, recoilID, reactionID, ignoreDefense, activeRagdoll, senselessTime, damageType); } else if(debug) { Debug.LogWarning($"({damageType}) vMeleeManager not found on {animator.name}. Cannot activate/deactivate damage."); } } } }