Move files and folders to new repo format to enable multi-project format
This commit is contained in:
268
Zennysoft.Game.Ma/src/enemy/Enemy.cs
Normal file
268
Zennysoft.Game.Ma/src/enemy/Enemy.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
using Chickensoft.AutoInject;
|
||||
using Chickensoft.Collections;
|
||||
using Chickensoft.Introspection;
|
||||
using Godot;
|
||||
using System.Linq;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user