using Cirno.Scripts; using Godot; using System; using System.Diagnostics; using Cirno.Scripts.Components; using Cirno.Scripts.Enums; using Godot.Collections; public partial class Enemy : CharacterBody2D { private InteractionController _cachedPlayer; public InteractionController CachedPlayer { get => _cachedPlayer; protected set => _cachedPlayer = value; } private EnemyState _currentState = EnemyState.Idle; [Export] public float Health = 4f; [Export] public float WalkSpeed = 2500f; [Export] public float PlayerDisengageRange = 500f; [Export] public float AlarmReactRange = 200f; [Export] public Weapon EquippedWeapon; [Export] public Node2D DefeatScript; [Export] public AiState Ai { get; private set; } [Export] public PackedScene CorpseTemplate { get; private set; } protected float _currentHealth = 0f; private bool _isDestroyed = false; private NavigationAgent2D _navigationAgent; protected bool _invulnerable = false; #region Manual Movement private Vector2 _movementDirection { get; set; } = Vector2.Zero; private Vector2 _facingDirection { get; set; } = Vector2.Right; #endregion private bool IsPlayerInRange => _playerDetection is { IsPlayerInRange: true }; private bool IsPlayerInSight => _playerDetection is not null && _playerDetection.IsPlayerInSight(CollisionMask); private Vector2? _lastPlayerPosition = null; [Export] private PlayerDetection _playerDetection; [Export] private bool _navigationEnabled = false; private AlarmManager _alarmManager; public bool NavigationEnabled { get => Ai is AiState.Enabled && _navigationEnabled && _navigationAgent != null; set => _navigationEnabled = value; } #region Events [Signal] public delegate void HealthChangedEventHandler(float newValue); #endregion // Called when the node enters the scene tree for the first time. public override void _Ready() { _currentHealth = Health; _navigationAgent = GetNodeOrNull("NavigationAgent2D"); _alarmManager = this.GetAlarmManager(); if (_alarmManager != null) { _alarmManager.AlarmEnabled += AlarmManagerOnAlarmEnabled; } } public override void _ExitTree() { if (_alarmManager == null) return; _alarmManager.AlarmEnabled -= AlarmManagerOnAlarmEnabled; } private void AlarmManagerOnAlarmEnabled(Vector2 location) { if (NavigationEnabled && location.DistanceTo(this.GlobalPosition) <= AlarmReactRange) { GD.Print($"Enemy {Name} alerted"); this._currentState = EnemyState.Alert; _lastPlayerPosition = location; } } // Called every frame. 'delta' is the elapsed time since the previous frame. public override void _Process(double delta) { // switch (_currentState) // { // case EnemyState.Idle: // // break; // case EnemyState.Alert: // break; // // //case EnemyState.Shooting: // // Shoot // //break; // default: // break; // } if (Ai is AiState.Controlled && !_isDestroyed) { _movementDirection = GetInput(); _facingDirection = _movementDirection; } } private Vector2 GetInput() { return Input.GetVector("left", "right", "up", "down"); } public override void _PhysicsProcess(double delta) { if (_isDestroyed) return; switch (Ai) { case AiState.Enabled: HandleAi(delta); return; case AiState.Controlled: HandleManualControl(delta); break; } } private void HandleManualControl(double delta) { Velocity = _movementDirection * (float)(WalkSpeed * delta); MoveAndSlide(); } private void HandleAi(double delta) { switch (_currentState) { case EnemyState.Idle: if (_playerDetection != null && _playerDetection.IsPlayerInSight(CollisionMask)) { _currentState = EnemyState.Alert; } //HandlePlayerDetection(); break; case EnemyState.Alert: // Update last known player position if it's in range if (IsPlayerInRange) { _lastPlayerPosition = _playerDetection.CachedPlayer.GlobalPosition; } if (NavigationEnabled) { if (_lastPlayerPosition.HasValue) { _navigationAgent.SetTargetPosition(_lastPlayerPosition.Value); } var currentAgentPosition = GlobalPosition; var nextPathPosition = _navigationAgent.GetNextPathPosition(); var newVelocity = currentAgentPosition.DirectionTo(nextPathPosition) * (float)(WalkSpeed * delta); // Navigation is over, can do other things like shooting if (_navigationAgent.IsNavigationFinished()) { // Shoot player if (IsPlayerInSight) { Shoot(); } // TODO: If player totally left the max range it should stop shooting and go back to idle return; } if (_navigationAgent.AvoidanceEnabled) { _navigationAgent.SetVelocity(newVelocity); } else { _on_navigation_agent_2d_velocity_computed(newVelocity); } MoveAndSlide(); } else { if (IsPlayerInSight) { Shoot(); } } break; case EnemyState.Patrolling: break; default: throw new ArgumentOutOfRangeException(); } } public void _on_navigation_agent_2d_velocity_computed(Vector2 safeVelocity) { this.Velocity = safeVelocity; } protected virtual void Shoot() { if (EquippedWeapon == null || !_lastPlayerPosition.HasValue) return; // Shoot at the player's last known position EquippedWeapon.ShootDirection = (_lastPlayerPosition.Value - this.GlobalPosition).Normalized(); EquippedWeapon.Shoot(); } private void _on_damage_hitbox_area_entered(Area2D area) { if (area is not Bullet bullet) return; if (!bullet.Enabled) return; if (_invulnerable) return; if (bullet.BulletInfo.Owner == BulletOwner.Enemy) return; this.Hit(bullet.Damage); bullet.RequestCollisionDestruction(); } // Bullets collision private void _on_area_entered(Area2D area) { } protected virtual void Explode() { Debug.WriteLine("Ded"); if (DefeatScript is not null) { ActivateDefeatScript(); } //CreateParticles(); CreateDebris(); QueueFree(); } private void CreateDebris() { if (CorpseTemplate is not null) { this.CreateSibling(CorpseTemplate); } } protected virtual void ActivateDefeatScript() { if (DefeatScript is not IActivable target) { GD.PrintErr($"Target {DefeatScript.Name} is not activable"); return; } target?.Activate(); GD.Print($"{DefeatScript.Name} activated"); } public void Hit(float damage) { if (_isDestroyed) return; if (_invulnerable) return; _currentHealth -= damage; EmitSignal(SignalName.HealthChanged, _currentHealth); if (!(_currentHealth <= 0)) return; _isDestroyed = true; Explode(); } public bool IsDestroyed() { return _isDestroyed; } public void AssumeControl() { GD.Print("Assuming direct control"); Ai = AiState.Controlled; } } public enum AiState { Enabled, Disabled, Controlled }