using System; using Godot; using Movementtests.interfaces; [GlobalClass] public partial class Enemy : CharacterBody3D, IDamageable, IDamageDealer, IHealthable, IKillable, IMoveable, ISpawnable, IKnockbackable, ITargetable { // 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 RHealth RHealth { get; set; } [Export] public RDeathEffect[] DeathEffects { get; set; } public IHealthable CHealth { get; set; } [ExportGroup("Damage")] [Export] public RDamage RDamage { get; set; } public IDamageable CDamageable { get; set; } [Export] public RKnockback RKnockback { get; set; } public IKnockbackable CKnockback { get; set; } [ExportGroup("Movement")] [Export] public RMovement RMovement { get; set; } public IMoveable CMovement { 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"); CDamageable = GetNode("CDamageable") as IDamageable; CMovement = GetNode("CMovement") as IMoveable; CHealth = GetNode("CHealth") as IHealthable; CKnockback = GetNode("CKnockback") as IKnockbackable; if (CDamageable is null) GD.PrintErr("This node needs a 'CDamage' child of type IDamageable!"); if (CMovement is null) GD.PrintErr("This node needs a 'CMovement' child of type IMoveable!"); if (CHealth is null) GD.PrintErr("This node needs a 'CHealth' child of type IHealthable!"); if (CKnockback is null) GD.PrintErr("This node needs a 'CKnockback' child of type IKnockbackable!"); if (RMovement != null) CMovement!.RMovement = RMovement; if (RHealth != null) { CHealth!.RHealth = RHealth; CHealth.CurrentHealth = RHealth.StartingHealth; } if (RKnockback != null) CKnockback!.RKnockback = RKnockback; } public void SetupSignals() { CDamageable.DamageTaken += ReduceHealth; CDamageable.DamageTaken += RegisterKnockback; CHealth.HealthDepleted += Kill; } public override void _PhysicsProcess(double delta) { // Only trigger gameplay related effects on specific frames if(Engine.GetPhysicsFrames() % 10 == 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 null) return Vector3.Zero; return CMovement.ComputeVelocity(inputs); } public DamageRecord TakeDamage(DamageRecord damageRecord) { if (CDamageable is null) return damageRecord with { Damage = new RDamage(0, damageRecord.Damage.DamageType) }; var finalDamage = CDamageable.TakeDamage(damageRecord); DamageTaken?.Invoke(this, finalDamage); return finalDamage; } public void ReduceHealth(IDamageable source, DamageRecord damageRecord) { if (CHealth is null) return; CHealth.ReduceHealth(source, damageRecord); HealthChanged?.Invoke(this, CHealth.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 null) return; CKnockback.RegisterKnockback(source, damageRecord); } public Vector3 ComputeKnockback() { if (CKnockback is null) return Vector3.Zero; return CKnockback.ComputeKnockback(); } public Vector3 GetTargetGlobalPosition() { var target = GetNode("CTarget"); if (target is null) return GlobalPosition; return target.GlobalPosition; } }