Rework enemy behavior (still in progress but shouldn't crash)

This commit is contained in:
2025-10-20 19:24:50 -07:00
parent 20b659681a
commit 44fd8c82b0
135 changed files with 2165 additions and 2415 deletions

View File

@@ -1,14 +1,15 @@
using Chickensoft.AutoInject;
using Chickensoft.Collections;
using Chickensoft.Introspection;
using Godot;
using System.Collections.Immutable;
using System.Linq;
using Zennysoft.Ma.Adapter;
using Zennysoft.Ma.Adapter.Entity;
namespace Zennysoft.Game.Ma;
[Meta(typeof(IAutoNode))]
public partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLogic>
public abstract partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLogic>
{
#region Registration
public override void _Notification(int what) => this.Notify(what);
@@ -21,50 +22,30 @@ public partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLogic>
#endregion
#region Dependencies
[Dependency] IGame Game => this.DependOn<IGame>();
[Dependency] protected IPlayer _player => this.DependOn(() => GetParent().GetChildren().OfType<IPlayer>().Single());
#endregion
#region Exports
[Export] protected EnemyStatResource _enemyStatResource { get; set; } = default!;
[Export]
protected float _movementSpeed = 0.5f;
#endregion
#region Node Dependencies
[Node] private CollisionShape3D _collisionShape { get; set; } = default!;
[Node] private Area3D LineOfSight { get; set; } = default!;
[Node] protected Timer _attackTimer { get; set; } = default!;
[Node] private RayCast3D Raycast { get; set; } = default!;
[Node] public IEnemyModelView EnemyModelView { get; set; } = default!;
[Node] private HealthComponent _healthComponent { get; set; } = default!;
#endregion
public AutoProp<double> CurrentHP { get; set; }
public virtual IEnemyModelView EnemyModelView { get; set; } = default!;
public string EnemyName;
private float _knockbackStrength = 0.0f;
private Vector3 _knockbackDirection = Vector3.Zero;
private DamageCalculator _damageCalculator;
public Vector3 TargetPosition { get; private set; }
#region Godot methods
public void Setup()
{
_enemyLogic = new EnemyLogic();
_enemyLogic.Set(_enemyStatResource);
_enemyLogic.Set(this as IEnemy);
_enemyLogic.Set(_player);
_damageCalculator = new DamageCalculator();
SetPhysicsProcess(true);
_healthComponent.HealthReachedZero += Die;
_healthComponent.HealthLowered += TakeHit;
EnemyModelView.HitPlayer += EnemyModelView_HitPlayer;
}
public void OnResolved()
@@ -72,143 +53,105 @@ public partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLogic>
EnemyBinding = _enemyLogic.Bind();
EnemyBinding
.Handle((in EnemyLogic.Output.TakeAction _) =>
.Handle((in EnemyLogic.Output.Activate _) =>
{
TakeAction();
Activate();
})
.Handle((in EnemyLogic.Output.Defeated output) =>
.Handle((in EnemyLogic.Output.Idle _) =>
{
Idle();
})
.Handle((in EnemyLogic.Output.Move _) =>
{
Move();
})
.Handle((in EnemyLogic.Output.ReturnToDefaultState _) =>
{
ReturnToDefaultState();
});
this.Provide();
_enemyLogic.Start();
CurrentHP = new AutoProp<double>(_enemyStatResource.MaximumHP);
CurrentHP.Sync += OnHPChanged;
LineOfSight.BodyEntered += LineOfSight_BodyEntered;
}
public override void _Process(double delta)
{
if (CurrentHP.Value <= 0)
return;
var lookDir = GlobalPosition + Velocity;
if (!lookDir.IsEqualApprox(GlobalPosition))
LookAt(lookDir, Vector3.Up, true);
EnemyModelView.SetCurrentDirection(GlobalBasis, -_player.CurrentBasis.Z);
}
#endregion
public virtual void TakeAction()
{
EnemyModelView.PlayPrimaryAttackAnimation();
}
public virtual void SetTarget(Vector3 target)
public virtual void Activate()
{
}
public virtual void Move(Vector3 velocity)
{
_knockbackStrength *= 0.9f;
Velocity = velocity * _movementSpeed + (_knockbackDirection * _knockbackStrength);
MoveAndSlide();
}
public virtual void TakeDamage(double damage, ElementType elementType, bool isCriticalHit = false, bool ignoreDefense = false, bool ignoreElementalResistance = false)
{
if (CurrentHP.Value > 0)
{
_damageCalculator.CalculateDamage(damage,
elementType,
_player.Stats.CurrentDefense.Value + _player.Stats.BonusDefense.Value,
_enemyStatResource.ElementalResistance,
isCriticalHit,
ignoreDefense,
ignoreElementalResistance);
GD.Print($"Enemy Hit for {damage} damage.");
CurrentHP.OnNext(CurrentHP.Value - damage);
GD.Print("Current HP: " + CurrentHP.Value);
if (CurrentHP.Value <= 0)
return;
EnemyModelView.PlayHitAnimation();
_enemyLogic.Input(new EnemyLogic.Input.Alerted());
if (this is ICanActivate activatable)
activatable.Activate();
if (((Weapon)_player.EquippedWeapon.Value).WeaponTag == WeaponTag.SelfDamage)
_player.Stats.SetCurrentHP(_player.Stats.CurrentHP.Value - 5);
}
}
public void Knockback(float impulse, Vector3 direction)
{
_knockbackDirection = direction;
_knockbackStrength = 0.3f;
}
public virtual void Die()
{
SetProcess(false);
_movementSpeed = 0;
CurrentHP.OnNext(0);
_enemyLogic.Input(new EnemyLogic.Input.EnemyDefeated());
_collisionShape.SetDeferred("disabled", true);
EnemyModelView.PlayDeathAnimation();
var tweener = CreateTween();
tweener.TweenInterval(1.0f);
tweener.TweenCallback(Callable.From(QueueFree));
Game.EnemyDefeated(GlobalPosition, _enemyStatResource);
}
public void SetCurrentHP(int targetHP)
{
CurrentHP.OnNext(targetHP);
}
public int GetMaximumHP()
{
return _enemyStatResource.MaximumHP;
}
public virtual void StartAttackTimer()
{
_attackTimer.Timeout += OnAttackTimeout;
}
public virtual void StopAttackTimer()
{
_attackTimer.Timeout -= OnAttackTimeout;
}
public void Idle()
public virtual void Idle()
{
EnemyModelView.PlayIdleAnimation();
}
public void Move()
public virtual void Move()
{
EnemyModelView.PlayWalkAnimation();
}
public Vector3 GetEnemyGlobalPosition() => GlobalPosition;
public void SetEnemyGlobalPosition(Vector3 target)
public virtual void PerformAction()
{
GlobalPosition = new Vector3(target.X, -0.5f, target.Z);
EnemyModelView.PlayPrimaryAttackAnimation();
}
public IDungeonRoom GetCurrentRoom()
public virtual void ReturnToDefaultState()
{
var currentRooms = Game.CurrentFloor.Rooms;
foreach (var room in currentRooms)
}
public void SetTarget(Vector3 targetPosition) => TargetPosition = targetPosition;
public virtual void SetEnemyPosition(Vector3 newPosition)
{
GlobalPosition = newPosition;
if (this is IHavePatrolBehavior patrolEnemy)
patrolEnemy.PatrolBehavior.HomePosition = GlobalPosition;
_enemyLogic.Input(new EnemyLogic.Input.Reset());
}
public void LookAtTarget(Vector3 targetPosition)
{
var lookDirection = GlobalPosition - targetPosition;
if (lookDirection != GlobalPosition)
LookAt(new Vector3(lookDirection.X, GlobalPosition.Y, lookDirection.Z), Vector3.Up);
}
public virtual void TakeDamage(int damage)
{
GD.Print($"Enemy Hit for {damage} damage.");
_healthComponent.CurrentHP.OnNext(_healthComponent.CurrentHP.Value - damage);
}
private void EnemyModelView_HitPlayer()
{
_player.TakeDamage(new Damage(30, ElementType.None, false, false, false));
}
public virtual void TakeHit()
{
_enemyLogic.Input(new EnemyLogic.Input.Alert());
EnemyModelView.PlayHitAnimation();
}
public virtual void Die()
{
SetPhysicsProcess(false);
_healthComponent.CurrentHP.OnCompleted();
_enemyLogic.Input(new EnemyLogic.Input.Defeated());
EnemyModelView.PlayDeathAnimation();
var tweener = CreateTween();
tweener.TweenInterval(1.0f);
tweener.TweenCallback(Callable.From(QueueFree));
}
public IDungeonRoom GetCurrentRoom(ImmutableList<IDungeonRoom> roomList)
{
foreach (var room in roomList)
{
var enemiesInCurrentRoom = room.EnemiesInRoom;
if (enemiesInCurrentRoom.Contains(this))
@@ -217,22 +160,6 @@ public partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLogic>
return null;
}
private void OnAttackTimeout()
{
if (GlobalPosition.DistanceTo(_player.CurrentPosition) > 5f)
{
_enemyLogic.Input(new EnemyLogic.Input.Alerted());
return;
}
var rng = new RandomNumberGenerator();
rng.Randomize();
_enemyLogic.Input(new EnemyLogic.Input.AttackTimer());
_attackTimer.Stop();
_attackTimer.WaitTime = rng.RandfRange(2f, 5.0f);
_attackTimer.Start();
}
private void LineOfSight_BodyEntered(Node3D body)
{
var overlappingBodies = LineOfSight.GetOverlappingBodies();
@@ -245,25 +172,11 @@ public partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLogic>
{
var collider = Raycast.GetCollider();
if (collider is IPlayer)
{
Raycast.DebugShapeCustomColor = Color.FromString("Purple", Colors.Purple);
_enemyLogic.Input(new EnemyLogic.Input.Alerted());
}
_enemyLogic.Input(new EnemyLogic.Input.Follow());
}
}
}
private void OnHPChanged(double newHP)
{
if (newHP <= 0)
Die();
}
private double CalculateDefenseResistance(double incomingDamage)
{
return Mathf.Max(incomingDamage - _enemyStatResource.CurrentDefense, 0.0);
}
public void OnExitTree()
{
_enemyLogic.Stop();