using System; using Godot; using Movementtests.interfaces; using Movementtests.systems; [GlobalClass, Icon("res://assets/ui/IconGodotNode/node_3D/icon_beetle.png")] public partial class Enemy : CharacterBody3D, IDamageable, IDamageDealer, IHealthable, IKillable, IMoveable, ISpawnable, IKnockbackable, ITargetable, IStunnable { // Signals and events public event Action DamageTaken; public event Action HealthChanged; public event Action HealthDepleted; // Public export components [Export] public Node3D Target { get; set; } [Export] public float EnemyHeight { get; set; } = 1f; [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 => CHealth.CurrentHealth; set => CHealth.CurrentHealth = value; } // Private stuff private Area3D _damageBox; private Node3D _target; private Healthbar _healthbar; public override void _Ready() { Initialize(); SetupSignals(); } public void Initialize() { _damageBox = GetNode("DamageBox"); _target = GetNode("CTarget"); 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!"); _healthbar = GetNode("CHealthBar").Healthbar; if (RMovement != null) CMovement!.RMovement = RMovement; if (RHealth != null) { CHealth!.RHealth = RHealth; CHealth.CurrentHealth = RHealth.StartingHealth; } if (RKnockback != null) CKnockback!.RKnockback = RKnockback; _healthbar.Initialize(CHealth!.CurrentHealth); } public void SetupSignals() { // Anonymous function call to erase return values of ReduceHealth CDamageable.DamageTaken += (source, record) => ReduceHealth(source, record); CDamageable.DamageTaken += (source, record) => RegisterKnockback(new KnockbackRecord(record)); CHealth.HealthDepleted += Kill; HealthChanged += (source, record) => _healthbar.SetHealth(record.CurrentHealth); } 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) { if (IsStunned) return; var bodies = _damageBox.GetOverlappingBodies(); foreach (var body in bodies) { if(body is IDamageable spawnable) spawnable.TakeDamage(new DamageRecord(GlobalPosition, 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 DamageRecord ComputeDamage(DamageRecord damageRecord) { if (CDamageable is null) return damageRecord with { Damage = new RDamage(0, damageRecord.Damage.DamageType) }; return CDamageable.ComputeDamage(damageRecord); } public HealthChangedRecord ReduceHealth(IDamageable source, DamageRecord damageRecord) { if (CHealth is null) return new HealthChangedRecord(0, 0, 0); var record = CHealth.ReduceHealth(source, damageRecord); HealthChanged?.Invoke(this, record); return record; } public void Kill(IHealthable source) { // Remove weapon that might be planted there foreach (var child in GetChildren()) { if (child is WeaponSystem system) { CallDeferred(Node.MethodName.RemoveChild, system); GetTree().GetRoot().CallDeferred(Node.MethodName.AddChild, system); system.CallDeferred(Node3D.MethodName.SetGlobalPosition, GlobalPosition + Vector3.Up*EnemyHeight); system.CallDeferred(WeaponSystem.MethodName.RethrowWeapon); } } foreach (var killable in DeathEffects.ToIKillables()) { killable.Kill(source); } CallDeferred(Node.MethodName.QueueFree); } public void RegisterKnockback(KnockbackRecord knockbackRecord) { if (CKnockback is null) return; CKnockback.RegisterKnockback(knockbackRecord); } public Vector3 ComputeKnockback() { if (CKnockback is null) return Vector3.Zero; return CKnockback.ComputeKnockback(); } public Vector3 GetTargetGlobalPosition() { if (_target is null) return GlobalPosition; return _target.GlobalPosition; } // Stun management public bool IsStunned { get; set; } = false; [Export(PropertyHint.Range, "0.1, 2, 0.1, or_greater")] public float StunDuration { get; set; } = 1f; public void Stun() { IsStunned = true; GetTree().CreateTimer(StunDuration).Timeout += Unstun; } public void Unstun() { IsStunned = false; } }