Rework enemy behavior (still in progress but shouldn't crash)
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user