Files
MovementTests/scenes/enemies/Enemy.cs

325 lines
8.9 KiB
C#

using System;
using System.Collections.Generic;
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Gamesmiths.Forge.Abilities;
using Gamesmiths.Forge.Attributes;
using Gamesmiths.Forge.Core;
using Gamesmiths.Forge.Cues;
using Gamesmiths.Forge.Effects;
using Gamesmiths.Forge.Events;
using Gamesmiths.Forge.Godot.Resources.Abilities;
using Gamesmiths.Forge.Statescript;
using Gamesmiths.Forge.Tags;
using Godot;
using Movementtests.interfaces;
using Movementtests.systems;
using Movementtests.tools;
using Node = Godot.Node;
[GlobalClass, Icon("res://assets/ui/IconGodotNode/node_3D/icon_beetle.png"), Meta(typeof(IAutoNode))]
public partial class Enemy : CharacterBody3D,
IDamageable,
IHealthable,
IKillable,
IMoveable,
ISpawnable,
IKnockbackable,
ITargetable,
IStunnable,
IForgeEntity
{
public override void _Notification(int what) => this.Notify(what);
#region Dependencies
[Dependency]
public TagsManager TagsManager => this.DependOn<TagsManager>();
[Dependency]
public CuesManager CuesManager => this.DependOn<CuesManager>();
#endregion
#region Signals
// Signals and events
public event Action<IDamageable, DamageRecord> DamageTaken = null!;
public event Action<IHealthable, HealthChangedRecord> HealthChanged = null!;
public event Action<IHealthable> HealthDepleted = null!;
#endregion
#region Inspector
// Public export components
[Export]
public Node3D? Target { get; set; }
[Export] public required ForgeAbilityData HitAbility { get; set; }
[Export]
public float EnemyHeight { get; set; } = 1f;
[ExportGroup("Health")]
[Export]
public RHealth RHealth { get; set; } = null!;
[Export]
public RDeathEffect[] DeathEffects { get; set; } = null!;
public IHealthable CHealth { get; set; } = null!;
[ExportGroup("Damage")]
[Export]
public RDamage? RDamage { get; set; }
public IDamageable CDamageable { get; set; } = null!;
[Export]
public RKnockback? RKnockback { get; set; }
public IKnockbackable CKnockback { get; set; } = null!;
[ExportGroup("Movement")]
[Export]
public required RMovement RMovement { get; set; }
public required IMoveable CMovement { get; set; }
#endregion
// Public stuff
public float CurrentHealth
{
get => CHealth.CurrentHealth;
set => CHealth.CurrentHealth = value;
}
#region IForgeEntity
// Perfectly forward the IForgeEntity interface to the ForgeEntity component
public EntityAttributes Attributes
{
get => ForgeEntity.Attributes;
set => ForgeEntity.Attributes = value;
}
public EntityTags Tags
{
get => ForgeEntity.Tags;
set => ForgeEntity.Tags = value;
}
public EffectsManager EffectsManager
{
get => ForgeEntity.EffectsManager;
set => ForgeEntity.EffectsManager = value;
}
public EntityAbilities Abilities
{
get => ForgeEntity.Abilities;
set => ForgeEntity.Abilities = value;
}
public EventManager Events
{
get => ForgeEntity.Events;
set => ForgeEntity.Events = value;
}
public Variables SharedVariables
{
get => ForgeEntity.SharedVariables;
set => ForgeEntity.SharedVariables = value;
}
#endregion
// Private stuff
[Node("DamageBox")] public required Area3D DamageBox { get; set;}
[Node("CTarget")] public required Node3D TargetComponent { get; set;}
[Node("CHealthBar")] public required CHealthbar HealthBarWrapper { get; set;}
[Node("ForgeEntityNode")] public required ForgeEntityNode ForgeEntity { get; set;}
private AbilityHandle? _hitAbilityHandle;
private EntityAttribute _healthAttribute;
public void OnReady()
{
Init();
SetupSignals();
}
public void Init()
{
CMovement = GetNode<Node>("CMovement") as IMoveable ?? throw new Exception("Movement component not found");
CMovement.RMovement = RMovement;
_healthAttribute = Attributes["EnemyAttributeSet.Health"];
CDamageable = (GetNode<Node>("CDamageable") as IDamageable)!;
CHealth = (GetNode<Node>("CHealth") as IHealthable)!;
CKnockback = (GetNode<Node>("CKnockback") as IKnockbackable)!;
CHealth.RHealth = RHealth;
CHealth.CurrentHealth = RHealth.StartingHealth;
CKnockback.RKnockback = RKnockback;
_hitAbilityHandle = Abilities.GrantAbilityPermanently(HitAbility.GetAbilityData(), 1, LevelComparison.None, this);
}
public void OnResolved()
{
HealthBarWrapper.ResourceBar.Init(_healthAttribute);
// CuesManager.RegisterCue(Tag.RequestTag(TagsManager, "cues.enemy.health"), HealthBarWrapper.ResourceBar);
Events.Subscribe(Tag.RequestTag(TagsManager, "events.combat.hit"),
data => {GD.Print("Hit!");});
Events.Subscribe(Tag.RequestTag(TagsManager, "events.combat.damage"), OnDamageReceived);
Events.Subscribe(Tag.RequestTag(TagsManager, "events.combat.death"), OnDeath);
}
public void SetupSignals()
{
// Anonymous function call to erase return values of ReduceHealth
// CDamageable.DamageTaken += (source, record) => ReduceHealth(source, record);
// CDamageable.DamageTaken += (_, record) => RegisterKnockback(new KnockbackRecord(record));
// 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)
{
if (IsStunned || _hitAbilityHandle == null) return;
var bodies = DamageBox.GetOverlappingBodies();
foreach (var body in bodies)
{
if (body is not IForgeEntity forgeEntity) continue;
var canActivate = _hitAbilityHandle.CanActivate(out var _);
if (!canActivate) return;
_hitAbilityHandle.Activate(out var _, forgeEntity);
}
}
public Vector3 ComputeVelocity(MovementInputs inputs)
{
return CMovement is null ? Vector3.Zero : CMovement.ComputeVelocity(inputs);
}
public void OnDamageReceived(EventData data)
{
var newHealth = _healthAttribute.CurrentValue + data.EventMagnitude;
if (newHealth > _healthAttribute.Min) return;
Events.Raise(new EventData
{
EventTags = Tag.RequestTag(TagsManager, "events.combat.death").GetSingleTagContainer()!,
Source = data.Source,
Target = data.Target
});
}
public void OnDeath(EventData data)
{
// Remove weapon that might be planted there
foreach (var child in GetChildren())
{
if (child is not WeaponSystem system) continue;
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);
}
CallDeferred(Node.MethodName.QueueFree);
}
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)
{
CKnockback.RegisterKnockback(knockbackRecord);
}
public Vector3 ComputeKnockback()
{
return CKnockback.ComputeKnockback();
}
public Vector3 GetTargetGlobalPosition()
{
return TargetComponent == null ? GlobalPosition : TargetComponent.GlobalPosition;
}
// Stun management
public bool IsStunned { get; set; }
[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;
}
}