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(); [Dependency] public CuesManager CuesManager => this.DependOn(); #endregion #region Signals // Signals and events public event Action DamageTaken = null!; public event Action HealthChanged = null!; public event Action 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("CMovement") as IMoveable ?? throw new Exception("Movement component not found"); CMovement.RMovement = RMovement; _healthAttribute = Attributes["EnemyAttributeSet.Health"]; CDamageable = (GetNode("CDamageable") as IDamageable)!; CHealth = (GetNode("CHealth") as IHealthable)!; CKnockback = (GetNode("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; } }