Refactor Enemy

This commit is contained in:
2025-02-06 01:58:36 -08:00
parent badc6d2375
commit 4e6c545e81
38 changed files with 581 additions and 589 deletions

View File

@@ -10,119 +10,123 @@ public partial class Enemy : RigidBody3D, IEnemy, IProvide<IEnemyLogic>
{
public override void _Notification(int what) => this.Notify(what);
public IEnemyLogic EnemyLogic { get; set; } = default!;
private IEnemyLogic _enemyLogic { get; set; } = default!;
IEnemyLogic IProvide<IEnemyLogic>.Value() => EnemyLogic;
IEnemyLogic IProvide<IEnemyLogic>.Value() => _enemyLogic;
public EnemyLogic.IBinding EnemyBinding { get; set; } = default!;
#region Dependencies
[Dependency] IGameRepo GameRepo => this.DependOn<IGameRepo>();
[Dependency] IGameEventDepot GameEventDepot => this.DependOn<IGameEventDepot>();
#endregion
[Export]
public EnemyStatResource EnemyStatResource { get; set; } = default!;
#region Exports
[Export] private EnemyStatResource _enemyStatResource { get; set; } = default!;
#endregion
public static PackedScene CollisionDetectorScene => GD.Load<PackedScene>("res://src/enemy/CollisionDetector.tscn");
#region Node Dependencies
[Node] private NavigationAgent3D NavAgent { get; set; } = default!;
public static Area3D CollisionDetector { get; set; } = default!;
[Node] private Area3D LineOfSight { get; set; } = default!;
public AutoProp<double> CurrentHP { get; set; }
[Node] private Timer PatrolTimer { get; set; } = default!;
[Node] public NavigationAgent3D NavAgent { get; set; } = default!;
[Node] private Timer AttackTimer { get; set; } = default!;
[Node] public Area3D LineOfSight { get; set; } = default!;
[Node] private RayCast3D Raycast { get; set; } = default!;
[Node] public Timer PatrolTimer { get; set; } = default!;
[Node] protected EnemyModelView EnemyModelView { get; set; } = default!;
#endregion
[Node] public Timer AttackTimer { get; set; } = default!;
public double CurrentHP => _currentHP.Value;
[Node] public RayCast3D Raycast { get; set; } = default!;
[Node] public EnemyModelView EnemyModelView { get; set; } = default!;
private AutoProp<double> _currentHP { get; set; }
private float _knockbackStrength = 0.0f;
private Vector3 _knockbackDirection = Vector3.Zero;
public void Setup()
{
EnemyLogic = new EnemyLogic();
EnemyLogic.Set(EnemyStatResource);
EnemyLogic.Set(this as IEnemy);
EnemyLogic.Set(GameRepo);
_enemyLogic = new EnemyLogic();
_enemyLogic.Set(_enemyStatResource);
_enemyLogic.Set(this as IEnemy);
_enemyLogic.Set(GameRepo);
}
public void OnReady()
{
SetPhysicsProcess(true);
CollisionDetector = CollisionDetectorScene.Instantiate<Area3D>();
CollisionDetector.AreaEntered += OnPlayerHitboxEntered;
AddChild(CollisionDetector);
EnemyModelView.Hitbox.AreaEntered += Hitbox_AreaEntered;
}
private void Hitbox_AreaEntered(Area3D area)
public void TakeDamage(double damage, ElementType elementType, bool isCriticalHit = false, bool ignoreDefense = false, bool ignoreElementalResistance = false)
{
if (area.GetParent().GetParent() is IPlayer player)
if (_currentHP.Value > 0)
{
var isCriticalHit = false;
var rng = new RandomNumberGenerator();
rng.Randomize();
var roll = rng.Randf();
if (roll <= EnemyStatResource.Luck)
isCriticalHit = true;
var damage = DamageCalculator.CalculateEnemyAttackDamage(
GameRepo.PlayerData.CurrentDefense.Value + GameRepo.PlayerData.BonusDefense,
EnemyStatResource,
GameRepo.PlayerData.Inventory.EquippedArmor.Value.ArmorStats,
isCriticalHit);
GameRepo.PlayerData.SetCurrentHP(GameRepo.PlayerData.CurrentHP.Value - Mathf.RoundToInt(damage));
GD.Print($"Player hit for {damage} damage.");
ApplyKnockback();
if (!ignoreElementalResistance)
damage = CalculateElementalResistance(damage, elementType);
if (!ignoreDefense)
damage = CalculateDefenseResistance(damage);
if (isCriticalHit)
damage *= 2;
GD.Print($"Enemy Hit for {damage} damage.");
_currentHP.OnNext(_currentHP.Value - damage);
GD.Print("Current HP: " + _currentHP.Value);
EnemyModelView.PlayHitAnimation();
_enemyLogic.Input(new EnemyLogic.Input.Alerted());
}
}
public void MoveToLocation(Vector3 target, float delta)
{
NavAgent.TargetPosition = target;
var targetPosition = NavAgent.GetNextPathPosition();
var velocity = (targetPosition - GlobalTransform.Origin).Normalized() * 2f * delta;
var lookAtDir = GlobalTransform.Origin - velocity;
var lookAtPosition = new Vector3(lookAtDir.X, GlobalPosition.Y, lookAtDir.Z);
if (GlobalPosition.DistanceTo(target) > 1.0f && !velocity.IsEqualApprox(Vector3.Zero) && !GlobalPosition.IsEqualApprox(lookAtPosition))
LookAt(lookAtPosition);
EnemyModelView.RotateModel(GlobalTransform.Basis, -GameRepo.PlayerGlobalTransform.Value.Basis.Z);
_knockbackStrength = _knockbackStrength * 0.9f;
MoveAndCollide(velocity + (_knockbackDirection * _knockbackStrength));
}
public void Die()
{
_enemyLogic.Input(new EnemyLogic.Input.EnemyDefeated());
EnemyModelView.PlayDeathAnimation();
var tweener = GetTree().CreateTween();
tweener.TweenInterval(1.0f);
tweener.TweenCallback(Callable.From(QueueFree));
GameEventDepot.OnEnemyDefeated(GlobalPosition, _enemyStatResource);
}
public void OnResolved()
{
EnemyBinding = EnemyLogic.Bind();
EnemyBinding = _enemyLogic.Bind();
EnemyBinding
.Handle((in EnemyLogic.Output.MovementComputed output) =>
.Handle((in EnemyLogic.Output.TakeAction _) =>
{
EnemyModelView.RotateModel(GlobalTransform.Basis, -GameRepo.PlayerGlobalTransform.Value.Basis.Z);
_knockbackStrength = _knockbackStrength * 0.9f;
MoveAndCollide(output.LinearVelocity + (_knockbackDirection * _knockbackStrength));
})
.Handle((in EnemyLogic.Output.HitByPlayer output) =>
{
EnemyModelView.PlayHitAnimation();
// TODO: Make this an event to notify game that player hit someone
if (GameRepo.PlayerData.Inventory.EquippedWeapon.Value.WeaponStats.WeaponTags.Contains(WeaponTag.SelfDamage))
GameRepo.PlayerData.SetCurrentHP(GameRepo.PlayerData.CurrentHP.Value - 5);
if (GameRepo.PlayerData.Inventory.EquippedWeapon.Value.WeaponStats.WeaponTags.Contains(WeaponTag.Knockback))
{
_knockbackDirection = -GameRepo.PlayerGlobalTransform.Value.Basis.Z.Normalized();
_knockbackStrength = 0.3f;
}
})
.Handle((in EnemyLogic.Output.Attack _) =>
{
EnemyModelView.PlayAttackAnimation();
TakeAction();
})
.Handle((in EnemyLogic.Output.Defeated output) =>
{
EnemyModelView.PlayDeathAnimation();
var tweener = GetTree().CreateTween();
tweener.TweenInterval(1.0f);
tweener.TweenCallback(Callable.From(QueueFree));
GameEventDepot.OnEnemyDefeated(GlobalPosition, EnemyStatResource);
Die();
});
this.Provide();
EnemyLogic.Start();
_enemyLogic.Start();
CurrentHP = new AutoProp<double>(EnemyStatResource.MaximumHP);
CurrentHP.Sync += OnHPChanged;
_currentHP = new AutoProp<double>(_enemyStatResource.MaximumHP);
_currentHP.Sync += OnHPChanged;
LineOfSight.BodyEntered += LineOfSight_BodyEntered;
PatrolTimer.Timeout += OnPatrolTimeout;
AttackTimer.Timeout += OnAttackTimeout;
@@ -133,7 +137,7 @@ public partial class Enemy : RigidBody3D, IEnemy, IProvide<IEnemyLogic>
public void OnExitTree()
{
EnemyLogic.Stop();
_enemyLogic.Stop();
EnemyBinding.Dispose();
}
@@ -142,32 +146,13 @@ public partial class Enemy : RigidBody3D, IEnemy, IProvide<IEnemyLogic>
var rng = new RandomNumberGenerator();
rng.Randomize();
var randomizedSpot = new Vector3(rng.RandfRange(-7.0f, 7.0f), 0, rng.RandfRange(-7.0f, 7.0f));
EnemyLogic.Input(new EnemyLogic.Input.PatrolToRandomSpot(GlobalPosition + randomizedSpot));
_enemyLogic.Input(new EnemyLogic.Input.PatrolToRandomSpot(GlobalPosition + randomizedSpot));
PatrolTimer.WaitTime = rng.RandfRange(5.0f, 10.0f);
}
public void OnPhysicsProcess(double delta)
{
EnemyLogic.Input(new EnemyLogic.Input.PhysicsTick(delta));
}
public void OnPlayerHitboxEntered(Area3D body)
{
if (body is IHitbox hitBox)
{
if (CurrentHP.Value > 0)
{
var isCriticalHit = false;
var rng = new RandomNumberGenerator();
rng.Randomize();
var roll = rng.Randf();
if (roll <= GameRepo.PlayerData.Inventory.EquippedWeapon.Value.WeaponStats.Luck)
isCriticalHit = true;
var damage = DamageCalculator.CalculateWeaponAttackDamage(GameRepo.PlayerData.CurrentAttack.Value + GameRepo.PlayerData.BonusAttack, EnemyStatResource, GameRepo.PlayerData.Inventory.EquippedWeapon.Value.WeaponStats, isCriticalHit);
GD.Print($"Enemy Hit for {damage} damage.");
EnemyLogic.Input(new EnemyLogic.Input.HitByPlayer(damage));
}
}
_enemyLogic.Input(new EnemyLogic.Input.PhysicsTick(delta));
}
private void OnAttackTimeout()
@@ -177,7 +162,7 @@ public partial class Enemy : RigidBody3D, IEnemy, IProvide<IEnemyLogic>
var rng = new RandomNumberGenerator();
rng.Randomize();
EnemyLogic.Input(new EnemyLogic.Input.AttackTimer());
_enemyLogic.Input(new EnemyLogic.Input.AttackTimer());
AttackTimer.Stop();
AttackTimer.WaitTime = rng.RandfRange(2f, 5.0f);
AttackTimer.Start();
@@ -197,20 +182,49 @@ public partial class Enemy : RigidBody3D, IEnemy, IProvide<IEnemyLogic>
if (collider is IPlayer player)
{
Raycast.DebugShapeCustomColor = Color.FromString("Purple", Colors.Purple);
EnemyLogic.Input(new EnemyLogic.Input.Alerted());
_enemyLogic.Input(new EnemyLogic.Input.Alerted());
}
}
}
}
private void ApplyKnockback()
{
if (GameRepo.PlayerData.Inventory.EquippedWeapon.Value.WeaponStats.WeaponTags.Contains(WeaponTag.Knockback))
{
_knockbackDirection = -GameRepo.PlayerGlobalTransform.Value.Basis.Z.Normalized();
_knockbackStrength = 0.3f;
}
}
private void OnHPChanged(double newHP)
{
if (newHP <= 0)
EnemyLogic.Input(new EnemyLogic.Input.EnemyDefeated());
Die();
}
private void DoNothing()
private double CalculateElementalResistance(double incomingDamage, ElementType incomingElementType)
{
if (incomingElementType == ElementType.Aeolic)
return Mathf.Max(incomingDamage - (incomingDamage * _enemyStatResource.AeolicResistance), 0.0);
if (incomingElementType == ElementType.Hydric)
return Mathf.Max(incomingDamage - (incomingDamage * _enemyStatResource.HydricResistance), 0.0);
if (incomingElementType == ElementType.Igneous)
return Mathf.Max(incomingDamage - (incomingDamage * _enemyStatResource.IgneousResistance), 0.0);
if (incomingElementType == ElementType.Ferrum)
return Mathf.Max(incomingDamage - (incomingDamage * _enemyStatResource.FerrumResistance), 0.0);
if (incomingElementType == ElementType.Telluric)
return Mathf.Max(incomingDamage - (incomingDamage * _enemyStatResource.TelluricResistance), 0.0);
return Mathf.Max(incomingDamage, 0.0);
}
private double CalculateDefenseResistance(double incomingDamage)
{
return Mathf.Max(incomingDamage - _enemyStatResource.CurrentDefense, 0.0);
}
public virtual void TakeAction()
{
}
}