ai/states/ai_engage_state.cs
// AiEngageState.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Sandbox;
public sealed class AiEngageState : AiState
{
public override string StateName => "Engage";
private enum CombatTactic
{
Approaching,
Circling,
Attacking,
Retreating
}
private CombatTactic _currentTactic = CombatTactic.Approaching;
private TimeSince _tacticTimer = 0f;
private TimeSince _attackCooldownTimer = 0f;
private bool _strafeRight = true;
private bool _isExecutingComboRoutine = false;
private float _combatWeight = 0f;
private readonly List<SkinnedModelRenderer> _renderers = new();
public override void OnEnter()
{
_currentTactic = CombatTactic.Approaching;
_tacticTimer = 0f;
_attackCooldownTimer = 0.8f;
_isExecutingComboRoutine = false;
_renderers.Clear();
_renderers.AddRange( Brain.Components.GetAll<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants ) );
Brain.Combat?.UpdateAnimWeaponType();
}
public override void OnUpdate()
{
if ( !Blackboard.HasValidTarget ) return;
_combatWeight = MathX.Lerp( _combatWeight, 1.0f, Time.Delta * 8f );
ApplyStance( true, _combatWeight );
if ( Combat.CurrentWeapon == null && Blackboard.NearestWeaponDrop != null && Blackboard.DistanceToTarget > 180f )
{
Brain.TransitionTo<AiSeekWeaponState>();
return;
}
var dist = Blackboard.DistanceToTarget;
var attackRange = MathF.Max( GetEffectiveAttackRange(), 85f );
var circleDistance = attackRange + 65f;
// Orientation : course face à la cible si loin, sinon face-à-face
if ( _currentTactic == CombatTactic.Approaching && dist > circleDistance && Brain.Agent.Velocity.LengthSquared > 400f )
{
var moveRot = Rotation.LookAt( Brain.Agent.Velocity.WithZ( 0 ).Normal, Vector3.Up );
Brain.WorldRotation = Rotation.Slerp( Brain.WorldRotation, moveRot, Time.Delta * 8f );
}
else
{
var lookRotation = Rotation.LookAt( Blackboard.DirectionToTarget.WithZ( 0 ), Vector3.Up );
Brain.WorldRotation = Rotation.Slerp( Brain.WorldRotation, lookRotation, Time.Delta * 10f );
}
var targetHead = Blackboard.Target.WorldPosition + Vector3.Up * 62f;
SetAimTarget( targetHead );
if ( !_isExecutingComboRoutine )
ExecuteTactic( dist, attackRange, circleDistance );
}
private void ExecuteTactic( float dist, float attackRange, float circleDistance )
{
switch ( _currentTactic )
{
case CombatTactic.Approaching:
Brain.Agent.MoveTo( Blackboard.Target.WorldPosition );
if ( dist <= circleDistance )
{
_currentTactic = CombatTactic.Circling;
_tacticTimer = 0f;
}
break;
case CombatTactic.Circling:
if ( _tacticTimer > 1.6f )
{
_strafeRight = Game.Random.Float() > 0.5f;
_tacticTimer = 0f;
}
var lateral = _strafeRight ? Brain.WorldRotation.Right : Brain.WorldRotation.Left;
var strafeDest = Blackboard.Target.WorldPosition + (lateral * 75f) - (Brain.WorldRotation.Forward * 20f);
Brain.Agent.MoveTo( strafeDest );
if ( _tacticTimer > 0.8f && _attackCooldownTimer > 1.6f )
{
_currentTactic = CombatTactic.Attacking;
_tacticTimer = 0f;
}
break;
case CombatTactic.Attacking:
Brain.Agent.MoveTo( Blackboard.Target.WorldPosition );
bool inRange = dist <= attackRange;
bool isBlocked = _tacticTimer > 0.35f && Brain.Agent.Velocity.Length < 25f && dist <= (attackRange * 1.25f);
if ( inRange || isBlocked )
{
Brain.Agent.Stop();
DecideAndExecuteStrike();
_attackCooldownTimer = 0f;
_currentTactic = CombatTactic.Retreating;
_tacticTimer = 0f;
}
else if ( _tacticTimer > 2.2f )
{
_currentTactic = CombatTactic.Circling;
_tacticTimer = 0f;
}
break;
case CombatTactic.Retreating:
var retreatPos = Brain.WorldPosition - (Brain.WorldRotation.Forward * 85f);
Brain.Agent.MoveTo( retreatPos );
if ( _tacticTimer > 0.7f || dist >= circleDistance )
{
_currentTactic = CombatTactic.Circling;
_tacticTimer = 0f;
}
break;
}
}
private void DecideAndExecuteStrike()
{
var targetFighter = Blackboard.TargetFighter;
var eyePos = Brain.WorldPosition + Vector3.Up * 60f;
var forward = Brain.WorldRotation.Forward;
var combo = Combat.ActiveProfile?.PrimaryCombo;
bool targetVulnerableToKick = targetFighter != null && (targetFighter.IsParrying || targetFighter.BalanceRatio < 0.30f);
if ( targetVulnerableToKick && Game.Random.Float() < 0.70f )
{
Combat.Kick( eyePos, forward );
return;
}
bool canDoDoubleCombo = combo != null && combo.Count >= 3;
bool targetStaggered = targetFighter != null && targetFighter.Status == FighterStatus.Staggered;
if ( canDoDoubleCombo && (targetStaggered || Game.Random.Float() < 0.40f) )
{
_ = ExecuteLinkedComboRoutineAsync( combo[1], combo[2], eyePos, forward );
return;
}
if ( combo != null && combo.Count > 0 )
{
Combat.ExecuteSpecificAttack( combo[0], eyePos, forward );
}
else
{
Combat.Attack( eyePos, forward );
}
}
private async Task ExecuteLinkedComboRoutineAsync( AttackProfile hit1, AttackProfile hit2, Vector3 eyePos, Vector3 forward )
{
_isExecutingComboRoutine = true;
Brain.Agent?.Stop();
Combat.ExecuteSpecificAttack( hit1, eyePos, forward );
if ( hit1.Cooldown > 0f )
await Task.Delay( (int)(hit1.Cooldown * 1000f) );
if ( !Brain.IsValid || Fighter == null || Fighter.Status != FighterStatus.Active )
{
_isExecutingComboRoutine = false;
return;
}
var updatedEyePos = Brain.WorldPosition + Vector3.Up * 60f;
var updatedForward = Blackboard.HasValidTarget ? Blackboard.DirectionToTarget : Brain.WorldRotation.Forward;
Combat.ExecuteSpecificAttack( hit2, updatedEyePos, updatedForward );
_isExecutingComboRoutine = false;
}
private float GetEffectiveAttackRange()
{
var profile = Combat.ActiveProfile?.PrimaryCombo;
if ( profile != null && profile.Count > 0 )
return profile[0].Range;
return 80f;
}
public override void OnExit()
{
Brain.Agent?.Stop();
_isExecutingComboRoutine = false;
_combatWeight = 0f;
ApplyStance( false, 0f );
}
private void ApplyStance( bool inCombat, float weight )
{
var currentWeaponType = (int)(Brain.Combat?.CurrentWeapon?.Type ?? WeaponType.Unarmed);
for ( int i = 0; i < _renderers.Count; i++ )
{
var rend = _renderers[i];
if ( !rend.IsValid ) continue;
rend.Set( "in_combat", inCombat );
rend.Set( "in_combat_weight", weight );
rend.Set( "look_weight", weight );
rend.Set( "weapon_type", currentWeaponType );
}
}
private void SetAimTarget( Vector3 targetPos )
{
for ( int i = 0; i < _renderers.Count; i++ )
{
var rend = _renderers[i];
if ( rend.IsValid )
rend.Set( "aim_target", targetPos );
}
}
public override string GetDebugStatus() => $"{StateName} [{_currentTactic}] Cible: {Blackboard.Target?.Name ?? "Aucune"}";
}