221 lines
6.0 KiB
C#
221 lines
6.0 KiB
C#
using System;
|
|
using Godot;
|
|
using Movementtests.interfaces;
|
|
using Movementtests.systems;
|
|
|
|
[GlobalClass]
|
|
public partial class Enemy : CharacterBody3D,
|
|
IDamageable,
|
|
IDamageDealer,
|
|
IHealthable,
|
|
IKillable,
|
|
IMoveable,
|
|
ISpawnable,
|
|
IKnockbackable,
|
|
ITargetable,
|
|
IStunnable
|
|
{
|
|
// Signals and events
|
|
public event Action<IDamageable, DamageRecord> DamageTaken;
|
|
public event Action<IHealthable, HealthChangedRecord> HealthChanged;
|
|
public event Action<IHealthable> 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<Area3D>("DamageBox");
|
|
_target = GetNode<Node3D>("CTarget");
|
|
|
|
CDamageable = GetNode<Node>("CDamageable") as IDamageable;
|
|
CMovement = GetNode<Node>("CMovement") as IMoveable;
|
|
CHealth = GetNode<Node>("CHealth") as IHealthable;
|
|
CKnockback = GetNode<Node>("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>("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;
|
|
}
|
|
}
|