using Chickensoft.AutoInject; using Chickensoft.Collections; using Chickensoft.Introspection; using Godot; using System.Linq; using Zennysoft.Ma.Adapter; namespace Zennysoft.Game.Ma; [Meta(typeof(IAutoNode))] public partial class Enemy : CharacterBody3D, IEnemy, IProvide { #region Registration public override void _Notification(int what) => this.Notify(what); protected IEnemyLogic _enemyLogic { get; set; } = default!; IEnemyLogic IProvide.Value() => _enemyLogic; public EnemyLogic.IBinding EnemyBinding { get; set; } = default!; #endregion #region Dependencies [Dependency] IGame Game => this.DependOn(); [Dependency] protected IPlayer _player => this.DependOn(() => GetParent().GetChildren().OfType().Single()); #endregion #region Exports [Export] protected EnemyStatResource _enemyStatResource { get; set; } = default!; [Export] protected float _movementSpeed = 2f; #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] protected IEnemyModelView _enemyModelView { get; set; } = default!; #endregion public AutoProp CurrentHP { get; set; } private float _knockbackStrength = 0.0f; private Vector3 _knockbackDirection = Vector3.Zero; private IDamageCalculator _damageCalculator; #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); } 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(_enemyStatResource.MaximumHP); CurrentHP.Sync += OnHPChanged; _lineOfSight.BodyEntered += LineOfSight_BodyEntered; } public void OnProcess(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); if (_enemyModelView is EnemyModelView2D enemyModelView2D) { if (_enemyLogic.Value is EnemyLogic.State.Patrolling or EnemyLogic.State.FollowPlayer) _enemyModelView.PlayWalkAnimation(); else _enemyModelView.PlayIdleAnimation(); } } #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) { _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 void Die() { SetProcess(false); 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 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 CalculateDefenseResistance(double incomingDamage) { return Mathf.Max(incomingDamage - _enemyStatResource.CurrentDefense, 0.0); } public void OnExitTree() { _enemyLogic.Stop(); EnemyBinding.Dispose(); } }