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 _potentialTargets = new(); private TimeSince _lastSearchRefresh; private TimeSince _stateEnterTime; public event Action 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() ) { 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(); } }