upd
This commit is contained in:
321
Code/AI/Enemy.cs
Normal file
321
Code/AI/Enemy.cs
Normal file
@@ -0,0 +1,321 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user