322 lines
7.3 KiB
C#
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();
|
|
}
|
|
}
|