using System; using Godot; using Movementtests.interfaces; [GlobalClass] public partial class Enemy : CharacterBody3D, IDamageable, IDamageDealer, IHealthable, IKillable, IMoveable, ISpawnable, IKnockbackable { // Signals and events public event Action DamageTaken; public event Action HealthChanged; public event Action HealthDepleted; // Public export components [Export] public Node3D Target { get; set; } [ExportGroup("Health")] [Export] public Node CHealth { get; set; } [Export] public RHealth RHealth { get; set; } [Export] public RDeathEffect[] DeathEffects { get; set; } [ExportGroup("Damage")] [Export] public Node CDamage { get; set; } [Export] public RDamage RDamage { get; set; } [Export] public Node CKnockback { get; set; } [Export] public RKnockback RKnockback { get; set; } [ExportGroup("Movement")] [Export] public Node CMovement { get; set; } [Export] public RMovement RMovement { get; set; } // Public stuff public float CurrentHealth { get; set; } // Private stuff private Area3D _damageBox; public override void _Ready() { Initialize(); SetupSignals(); } public void Initialize() { _damageBox = GetNode("DamageBox"); if (CMovement is IMoveable moveable && RMovement != null) moveable.RMovement = RMovement; if (CHealth is IHealthable healthable && RHealth != null) { healthable.RHealth = RHealth; healthable.CurrentHealth = RHealth.StartingHealth; } if (CKnockback is IKnockbackable knockbackable && RKnockback != null) knockbackable.RKnockback = RKnockback; } public void SetupSignals() { if (CDamage is IDamageable damageable) { damageable.DamageTaken += ReduceHealth; damageable.DamageTaken += RegisterKnockback; } if (CHealth is IHealthable healthable) healthable.HealthDepleted += Kill; } public override void _PhysicsProcess(double delta) { // Only trigger gameplay related effects 4 times per second if(Engine.GetPhysicsFrames() % 15 == 0) ProcessGameplay(delta); var targetPlanar = new Vector3(Target.GlobalPosition.X, GlobalPosition.Y, Target.GlobalPosition.Z); LookAt(targetPlanar); var inputs = new MovementInputs( Velocity: Velocity, TargetLocation: Target.GlobalPosition, isOnFloor: IsOnFloor(), gravity: GetGravity(), delta: delta ); Velocity = ComputeVelocity(inputs); Velocity += ComputeKnockback(); MoveAndSlide(); } public void ProcessGameplay(double delta) { var bodies = _damageBox.GetOverlappingBodies(); foreach (var body in bodies) { if(body is IDamageable spawnable) spawnable.TakeDamage(new DamageRecord(this, RDamage)); } } public Vector3 ComputeVelocity(MovementInputs inputs) { if (CMovement is not IMoveable movement) return Vector3.Zero; return movement!.ComputeVelocity(inputs); } public DamageRecord TakeDamage(DamageRecord damageRecord) { if (CDamage is not IDamageable damageable) return damageRecord with { Damage = new RDamage(0, damageRecord.Damage.DamageType) }; var finalDamage = damageable.TakeDamage(damageRecord); DamageTaken?.Invoke(this, finalDamage); return finalDamage; } public void ReduceHealth(IDamageable source, DamageRecord damageRecord) { if (CHealth is not IHealthable healthable) return; healthable.ReduceHealth(source, damageRecord); HealthChanged?.Invoke(this, healthable.CurrentHealth); } public void Kill(IHealthable source) { foreach (var killable in DeathEffects.ToIKillables()) { killable.Kill(source); } QueueFree(); } public void RegisterKnockback(IDamageable source, DamageRecord damageRecord) { if (CKnockback is not IKnockbackable knockbackable) return; knockbackable.RegisterKnockback(source, damageRecord); } public Vector3 ComputeKnockback() { if (CKnockback is not IKnockbackable knockbackable) return Vector3.Zero; return knockbackable.ComputeKnockback(); } }