using System; using System.Collections; using System.Collections.Generic; using Beyond; // For Player, GameStateManager, TimeController using Invector.vCharacterController.AI.FSMBehaviour; // For vFSMBehaviourController using UnityEngine; namespace Invector.vMelee { using vEventSystems; // For vIAttackListener public class bMeleeAttackControl : 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; 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" }; [Header("Hit Effects")] public bool ignoreDefense; public bool activeRagdoll; [vHideInInspector("activeRagdoll")] [Tooltip("Time to keep Ragdoll active if activeRagdoll is true.")] public float senselessTime; [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.5f; [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. Analogous to slowMoActivationDistance in vMeleeAttackControl.")] public float maxTargetDistance = 3f; // This will be used as slowMoActivationDistance [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. Analogous to rotationActivationAngle in vMeleeAttackControl.")] public float degreeThreshold = 20f; // This will be used as rotationActivationAngle // rotationSpeed field is now unused, AutoTargetting.playerRotationSpeed will be used. // maxTurnTowardDistance field is now unused. [Header("Debug")] public bool debug; // Private state variables private bool isActive; // Is damage window active private vIAttackListener mFighter; private bool isAttacking; // Is this attack state logic considered "attacking" 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}. Rotation and target-dependent slow-mo will be limited."); } isAttacking = true; isActive = false; m_hasScaledTime = false; if (mFighter != null) mFighter.OnEnableAttack(); if (debug) Debug.Log($"({damageType}) OnStateEnter: {animator.name}, Layer: {layerIndex}"); if (attackTimeScaleStart < 0f) attackTimeScaleStart = startDamage; if (attackTimeScaleEnd < 0f) attackTimeScaleEnd = endDamage; } override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (Player.Instance.ActiveWeaponTrail) // Existing weapon trail logic { Player.Instance.ActiveWeaponTrail.m_colorMultiplier = Color.white + Color.red * damageMultiplier; } float currentNormalizedTime = stateInfo.normalizedTime % 1; if (currentNormalizedTime == 0 && stateInfo.normalizedTime > 0.5f) currentNormalizedTime = 1f; // --- ROTATION LOGIC --- AttemptRotationTowardsAutoTarget(animator); // --- SLOW MOTION LOGIC --- if (useAttackTimeScale) { 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); // Note: Original bMeleeAttackControl reset m_hasScaledTime here. // vMeleeAttackControl resets it if currentNormalizedTime > attackTimeScaleEnd. // TimeController handles restoring time, so m_hasScaledTime is more about preventing re-triggering within one state. // It's reset in UpdateSlowMotion or OnStateExit. } // --- ATTACK STATE AND TRIGGER RESET LOGIC --- if (isAttacking) { if (currentNormalizedTime > endDamage) { if (mFighter != null) mFighter.OnDisableAttack(); isAttacking = false; if (debug) Debug.Log($"({damageType}) OnDisableAttack called: normTime={currentNormalizedTime:F2}"); } // Original bMelee had: "if (nTime > .1f && nTime < resetTriggerBeforeTime && isAttacking)" // vMelee has: "else if (resetAttackTrigger && currentNormalizedTime >= resetTriggerBeforeTime)" // Adopting vMelee's more standard approach for early reset: else if (resetAttackTrigger && currentNormalizedTime >= resetTriggerBeforeTime) { if (mFighter != null) mFighter.ResetAttackTriggers(); if (debug) Debug.Log($"({damageType}) ResetAttackTriggers called: normTime={currentNormalizedTime:F2}"); // To prevent multiple calls, ideally ResetAttackTriggers is idempotent or use a flag. // For now, matching vMelee's potential for multiple calls if time hovers. } } } override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (debug) Debug.Log($"({damageType}) OnStateExit: {animator.name}"); if (isActive) { isActive = false; ActiveDamage(animator, false); if (debug) Debug.Log($"({damageType}) Damage disabled on StateExit."); } if (isAttacking && mFighter != null) { mFighter.OnDisableAttack(); if (debug) Debug.Log($"({damageType}) OnDisableAttack called on StateExit (fallback)."); } isAttacking = false; m_hasScaledTime = false; // Reset slow motion flag if (mFighter != null && resetAttackTrigger) // Final reset if configured { mFighter.ResetAttackTriggers(); if (debug) Debug.Log($"({damageType}) ResetAttackTriggers called on StateExit due to resetAttackTrigger flag."); } } private void AttemptRotationTowardsAutoTarget(Animator animator) { if (!rotatePlayerTowardsTarget || _autoTargettingInstance == null || _autoTargettingInstance.CurrentTarget == null) { return; } // Using bMeleeAttackControl's degreeThreshold as the activation angle if (_autoTargettingInstance.IsTargetInAngle(animator.transform, _autoTargettingInstance.CurrentTarget, degreeThreshold)) { // AutoTargetting.playerRotationSpeed will be used internally by this call _autoTargettingInstance.ExecuteRotationTowardsCurrentTarget(Time.deltaTime); } } private void UpdateSlowMotion(Animator animator, AnimatorStateInfo stateInfo, float currentNormalizedTime) { // This method is called only if useAttackTimeScale (field) is true. if (_autoTargettingInstance == null || TimeController.Instance == null) return; if (!m_hasScaledTime) { if (currentNormalizedTime >= attackTimeScaleStart && currentNormalizedTime <= attackTimeScaleEnd) { bool triggerSlowMo = false; if (_autoTargettingInstance.CurrentTarget != null) { // Use bMeleeAttackControl's maxTargetDistance as the activation distance float distSqr = (_autoTargettingInstance.CurrentTarget.transform.position - animator.transform.position).sqrMagnitude; bool targetNear = distSqr <= maxTargetDistance * maxTargetDistance; if (targetNear) { // Mimicking vMeleeAttackControl's effective logic: // If useAttackTimeScale (field) is true (which it is to get here) and target is near, then trigger. // The lowHealthTh can act as an additional, prioritized condition if desired, // but with current structure, `this.useAttackTimeScale` being true makes the second part of OR true. float currentTargetHealth = _autoTargettingInstance.GetCurrentTargetHealth(); bool targetHealthLow = currentTargetHealth > 0f && currentTargetHealth < lowHealthTh; if (targetHealthLow) // Prioritize if health is low and near { triggerSlowMo = true; } // else if (this.useAttackTimeScale) // This refers to the field, which is true if we are in this function. // So if target is Near, this path will be taken if not already low health. // Simplified: if targetNear, triggerSlowMo = true because this.useAttackTimeScale is already true. // The following `else if` is essentially `else if (true)` else { triggerSlowMo = true; // General case: near and useAttackTimeScale is on } } } // else: No current target, so no target-dependent slow motion. if (triggerSlowMo) { // Use vMeleeAttackControl's duration calculation for SetTimeScaleForSec float slowMoEffectDuration = (attackTimeScaleEnd - currentNormalizedTime) * stateInfo.length; if (slowMoEffectDuration > 0.01f) // Ensure a meaningful duration { // The 'true' for forceUnique in bMelee's original TimeController call. // Assuming TimeController.Instance.SetTimeScaleForSec now handles this or has an overload. // If SetTimeScaleForSec(scale, duration, bool forceUnique) exists: // TimeController.Instance.SetTimeScaleForSec(attackTimeScale, slowMoEffectDuration, true); // If not, use the existing TimeController method. For now, assuming vMelee's version: 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 { m_hasScaledTime = false; if (debug) Debug.Log($"({damageType}) Slow-mo window ended (normTime={currentNormalizedTime:F2}). m_hasScaledTime reset."); // TimeController.Instance is responsible for restoring time scale. } } void ActiveDamage(Animator animator, bool value) { var meleeManager = animator.GetComponent(); 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."); } } } }