270 lines
7.4 KiB
C#
270 lines
7.4 KiB
C#
using Chickensoft.AutoInject;
|
|
using Chickensoft.Collections;
|
|
using Chickensoft.Introspection;
|
|
using Godot;
|
|
using System.Linq;
|
|
using Zennysoft.Ma.Godot.Adapter;
|
|
|
|
namespace Zennysoft.Game.Ma;
|
|
|
|
[Meta(typeof(IAutoNode))]
|
|
public partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLogic>
|
|
{
|
|
#region Registration
|
|
public override void _Notification(int what) => this.Notify(what);
|
|
|
|
protected IEnemyLogic _enemyLogic { get; set; } = default!;
|
|
|
|
IEnemyLogic IProvide<IEnemyLogic>.Value() => _enemyLogic;
|
|
|
|
public EnemyLogic.IBinding EnemyBinding { get; set; } = default!;
|
|
#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]
|
|
private float _movementSpeed = 2f;
|
|
#endregion
|
|
|
|
#region Node Dependencies
|
|
[Node] private CollisionShape3D _collisionShape { get; set; } = default!;
|
|
|
|
[Node] private Area3D _lineOfSight { get; set; } = default!;
|
|
|
|
[Node] private Timer _attackTimer { get; set; } = default!;
|
|
|
|
[Node] private RayCast3D _raycast { get; set; } = default!;
|
|
|
|
[Node] protected IEnemyModelView _enemyModelView { get; set; } = default!;
|
|
#endregion
|
|
|
|
public double CurrentHP => _currentHP.Value;
|
|
|
|
private AutoProp<double> _currentHP { get; set; }
|
|
|
|
private float _knockbackStrength = 0.0f;
|
|
|
|
private Vector3 _knockbackDirection = Vector3.Zero;
|
|
|
|
#region Godot methods
|
|
public void Setup()
|
|
{
|
|
_enemyLogic = new EnemyLogic();
|
|
_enemyLogic.Set(_enemyStatResource);
|
|
_enemyLogic.Set(this as IEnemy);
|
|
_enemyLogic.Set(_player);
|
|
}
|
|
|
|
public void OnResolved()
|
|
{
|
|
EnemyBinding = _enemyLogic.Bind();
|
|
|
|
EnemyBinding
|
|
.Handle((in EnemyLogic.Output.TakeAction _) =>
|
|
{
|
|
TakeAction();
|
|
})
|
|
.Handle((in EnemyLogic.Output.Defeated output) =>
|
|
{
|
|
});
|
|
|
|
this.Provide();
|
|
|
|
_enemyLogic.Start();
|
|
|
|
_currentHP = new AutoProp<double>(_enemyStatResource.MaximumHP);
|
|
_currentHP.Sync += OnHPChanged;
|
|
_lineOfSight.BodyEntered += LineOfSight_BodyEntered;
|
|
}
|
|
|
|
public override void _PhysicsProcess(double delta)
|
|
{
|
|
if (CurrentHP <= 0)
|
|
return;
|
|
|
|
var lookDir = GlobalPosition + Velocity;
|
|
if (!lookDir.IsEqualApprox(GlobalPosition) || !Velocity.IsZeroApprox())
|
|
LookAt(lookDir, Vector3.Up, true);
|
|
|
|
var isWalking = _enemyLogic.Value is EnemyLogic.State.Patrolling or EnemyLogic.State.FollowPlayer;
|
|
if (_enemyModelView is EnemyModelView2D enemyModelView2D)
|
|
enemyModelView2D.RotateModel(GlobalTransform.Basis, -_player.CurrentBasis.Z, isWalking);
|
|
}
|
|
#endregion
|
|
|
|
public virtual void TakeAction()
|
|
{
|
|
}
|
|
|
|
public virtual void SetTarget(Vector3 target)
|
|
{
|
|
|
|
}
|
|
|
|
public virtual void Move(Vector3 velocity)
|
|
{
|
|
_knockbackStrength = _knockbackStrength * 0.9f;
|
|
Velocity = velocity + (_knockbackDirection * _knockbackStrength);
|
|
MoveAndSlide();
|
|
}
|
|
|
|
public virtual void TakeDamage(double damage, ElementType elementType, bool isCriticalHit = false, bool ignoreDefense = false, bool ignoreElementalResistance = false)
|
|
{
|
|
if (_currentHP.Value > 0)
|
|
{
|
|
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);
|
|
|
|
if (_currentHP.Value <= 0)
|
|
return;
|
|
|
|
_enemyModelView.PlayHitAnimation();
|
|
_enemyLogic.Input(new EnemyLogic.Input.Alerted());
|
|
|
|
if (_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 void Die()
|
|
{
|
|
_currentHP.OnNext(0);
|
|
_enemyLogic.Input(new EnemyLogic.Input.EnemyDefeated());
|
|
_collisionShape.SetDeferred("disabled", true);
|
|
_enemyModelView.PlayDeathAnimation();
|
|
var tweener = GetTree().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 void StartAttackTimer()
|
|
{
|
|
_attackTimer.Timeout += OnAttackTimeout;
|
|
}
|
|
|
|
public void StopAttackTimer()
|
|
{
|
|
_attackTimer.Timeout -= OnAttackTimeout;
|
|
}
|
|
|
|
public Vector3 GetEnemyGlobalPosition() => GlobalPosition;
|
|
|
|
public void SetEnemyGlobalPosition(Vector3 target)
|
|
{
|
|
GlobalPosition = new Vector3(target.X, -0.5f, target.Z);
|
|
}
|
|
|
|
public IDungeonRoom GetCurrentRoom()
|
|
{
|
|
var currentRooms = Game.CurrentFloor.Rooms;
|
|
foreach (var room in currentRooms)
|
|
{
|
|
var enemiesInCurrentRoom = room.EnemiesInRoom;
|
|
if (enemiesInCurrentRoom.Contains(this))
|
|
return room;
|
|
}
|
|
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();
|
|
foreach (var _ in overlappingBodies)
|
|
{
|
|
if (_raycast.GlobalPosition != _player.CurrentPosition)
|
|
_raycast.LookAt(_player.CurrentPosition, Vector3.Up);
|
|
_raycast.ForceRaycastUpdate();
|
|
if (_raycast.IsColliding())
|
|
{
|
|
var collider = _raycast.GetCollider();
|
|
if (collider is IPlayer)
|
|
{
|
|
_raycast.DebugShapeCustomColor = Color.FromString("Purple", Colors.Purple);
|
|
_enemyLogic.Input(new EnemyLogic.Input.Alerted());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnHPChanged(double newHP)
|
|
{
|
|
if (newHP <= 0)
|
|
Die();
|
|
}
|
|
|
|
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 void OnExitTree()
|
|
{
|
|
_enemyLogic.Stop();
|
|
EnemyBinding.Dispose();
|
|
}
|
|
}
|