sasalka/Code/AI/Enemy.cs
2025-06-08 23:47:32 +03:00

322 lines
7.3 KiB
C#

using Sandbox;
using Sandbox.Citizen;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public sealed partial class Enemy : Component
{
public enum EnemyState
{
Idle,
Chase,
Attack,
Patrol,
Death
}
[Property, Sync] public int Health { get; set; } = 100;
[Property] public float MaxSpeed { get; set; } = 400;
[Property] public NavMeshAgent agent;
[Property] public SkinnedModelRenderer renderer;
[Property] public CitizenAnimationHelper AnimationHelper;
[Property, Sync] public EnemyState CurrentState { get; set; } = EnemyState.Idle;
[Property] public float DetectionRadius = 1000f;
[Property] public float AttackRadius = 80f;
[Property] public float AttackCooldown = 1f;
[Property] public RagdollController RagdollController;
[Property] public SoundPointComponent HitSound { get; set; }
[Property] public GameObject bloodParticle { get; set; }
private TimeSince _timeSinceLastAttack;
private Dedugan _currentTarget;
private TimeSince _lastTargetCheck;
private const float TargetCheckInterval = 1.5f;
private List<Dedugan> _potentialTargets = new();
private TimeSince _lastSearchRefresh;
private TimeSince _stateEnterTime;
public event Action<EnemyState, EnemyState> OnStateChanged;
protected override void OnStart()
{
renderer.Set( "scale_height", 2f );
_timeSinceLastAttack = 0;
_lastTargetCheck = 0;
RefreshPotentialTargets();
SwitchState( EnemyState.Idle );
// if ( !Network.IsOwner ) return;
if ( renderer is null )
return;
renderer.OnFootstepEvent += OnFootstepEvent;
}
protected override void OnUpdate()
{
// Всегда проверяем ближайшую цель
if ( _lastTargetCheck > TargetCheckInterval && CurrentState != EnemyState.Death )
{
FindClosestTarget();
_lastTargetCheck = 0;
}
switch ( CurrentState )
{
case EnemyState.Idle:
UpdateIdleState();
break;
case EnemyState.Chase:
UpdateChaseState();
break;
case EnemyState.Attack:
UpdateAttackState();
break;
case EnemyState.Patrol:
UpdatePatrolState();
break;
case EnemyState.Death:
UpdateDeathState();
break;
}
}
private void SwitchState( EnemyState newState )
{
if ( CurrentState == newState ) return;
if ( CurrentState == EnemyState.Death ) return;
var previousState = CurrentState;
CurrentState = newState;
_stateEnterTime = 0;
OnStateChanged?.Invoke( previousState, newState );
switch ( newState )
{
case EnemyState.Idle:
agent.Stop();
AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.None;
AnimationHelper.WithVelocity( Vector3.Zero );
renderer.Set( "scale_height", 2f );
break;
case EnemyState.Chase:
agent.MaxSpeed = MaxSpeed;
AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.Punch;
break;
case EnemyState.Patrol:
agent.MaxSpeed = MaxSpeed / 4;
break;
case EnemyState.Death:
EnterDeathState();
break;
}
}
private void RefreshPotentialTargets()
{
_potentialTargets.Clear();
foreach ( var player in Scene.Components.GetAll<Dedugan>() )
{
if ( player.IsValid() && player.Health > 0 )
{
_potentialTargets.Add( player );
}
}
_lastSearchRefresh = 0;
}
private void FindClosestTarget()
{
// Обновляем список целей при необходимости
if ( _lastSearchRefresh > 1f ) RefreshPotentialTargets();
Dedugan closestTarget = null;
float closestDistance = float.MaxValue;
foreach ( var target in _potentialTargets )
{
if ( !target.IsValid() || target.Health <= 0 ) continue;
float distance = Vector3.DistanceBetween( WorldPosition, target.WorldPosition );
if ( distance <= DetectionRadius && distance < closestDistance )
{
closestDistance = distance;
closestTarget = target;
}
else
{
_currentTarget = null;
}
}
// Если нашли более близкую цель, переключаемся на нее
if ( closestTarget != null && closestTarget != _currentTarget )
{
_currentTarget = closestTarget;
// Если мы в состоянии атаки/погони и цель сменилась, перезапускаем движение
if ( CurrentState == EnemyState.Chase || CurrentState == EnemyState.Attack )
{
agent.MoveTo( _currentTarget.WorldPosition );
}
}
}
private bool HasValidTarget()
{
return _currentTarget != null &&
_currentTarget.IsValid() &&
_currentTarget.Health > 0;
}
private void UpdateIdleState()
{
if ( HasValidTarget() )
{
Log.Info(_currentTarget);
SwitchState( EnemyState.Chase );
return;
}
if ( new Random().Next( 0, 100 ) > 90 )
{
SwitchState( EnemyState.Patrol );
}
SwitchState( EnemyState.Patrol );
AnimationHelper.WithVelocity( Vector3.Zero );
}
private void UpdatePatrolState()
{
if ( HasValidTarget() )
{
SwitchState( EnemyState.Chase );
return;
}
if ( agent.Velocity.Length <= 10 )
{
agent.MoveTo( Scene.NavMesh.GetRandomPoint() ?? WorldPosition );
}
AnimationHelper.WithVelocity( agent.Velocity );
}
private void UpdateChaseState()
{
if ( !HasValidTarget() )
{
SwitchState( EnemyState.Idle );
return;
}
float distance = Vector3.DistanceBetween( _currentTarget.WorldPosition, WorldPosition );
if ( distance <= AttackRadius )
{
SwitchState( EnemyState.Attack );
return;
}
if ( distance > DetectionRadius )
{
SwitchState( EnemyState.Idle );
return;
}
agent.MoveTo( _currentTarget.WorldPosition );
AnimationHelper.WithVelocity( agent.Velocity );
}
private void UpdateAttackState()
{
if ( !HasValidTarget() )
{
SwitchState( EnemyState.Idle );
return;
}
float distance = Vector3.DistanceBetween( _currentTarget.WorldPosition, WorldPosition );
if ( distance > AttackRadius )
{
SwitchState( EnemyState.Chase );
return;
}
agent.Stop();
AnimationHelper.WithVelocity( Vector3.Zero );
if ( _timeSinceLastAttack >= AttackCooldown )
{
var dir = _currentTarget.WorldPosition - WorldPosition;
renderer.Set( "b_attack", true );
_timeSinceLastAttack = 0;
_currentTarget.ReportHit( dir, 1, 15 );
CreateHitEffects( _currentTarget.WorldPosition + _currentTarget.WorldRotation.Up * 60, dir );
}
}
private void EnterDeathState()
{
agent.Stop();
AnimationHelper.WithVelocity( Vector3.Zero );
if ( RagdollController != null ) RagdollController.Enabled = true;
}
private void UpdateDeathState()
{
// Логика состояния смерти
}
public void Die()
{
SwitchState( EnemyState.Death );
DestroyAsync( GameObject, 5f );
}
[Rpc.Broadcast]
public void ReportHit( Vector3 dir, int boneIndex, int damage = 25 )
{
renderer.Set( "hit", true );
renderer.Set( "hit_bone", boneIndex );
renderer.Set( "hit_direction", dir );
renderer.Set( "hit_offset", dir );
renderer.Set( "hit_strength", 5 );
Health -= damage;
if ( Health <= 0 )
{
Die();
}
}
[Rpc.Broadcast]
private void CreateHitEffects( Vector3 position, Vector3 normal )
{
if ( HitSound != null ) HitSound.StartSound();
if ( bloodParticle != null )
{
var rot = Rotation.LookAt( normal );
DestroyAsync( bloodParticle.Clone( position, rot ), 0.5f );
}
}
async void DestroyAsync( GameObject go, float delay )
{
await GameTask.DelaySeconds( delay );
if ( go != null && go.IsValid() ) go.Destroy();
}
}