278 lines
15 KiB
C#
278 lines
15 KiB
C#
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<string>
|
|
|
|
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<string> bodyParts = new List<string> { "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<vIAttackListener>();
|
|
|
|
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<vMeleeManager>(); // 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.");
|
|
}
|
|
}
|
|
}
|
|
} |