Files
GameJamDungeon/Zennysoft.Game.Ma/src/enemy/Enemy.cs
2025-03-08 13:49:46 -08:00

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();
}
}