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 { #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] 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 _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(_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(); } }