using Chickensoft.AutoInject; using Chickensoft.Collections; using Chickensoft.GodotNodeInterfaces; using Chickensoft.Introspection; using Godot; using System; using System.Linq; namespace GameJamDungeon; public interface IEnemy : ICharacterBody3D { public IEnemyLogic EnemyLogic { get; } public AutoProp CurrentHP { get; set; } public EnemyStatInfo EnemyStatInfo { get; set; } public NavigationAgent3D NavAgent { get; set; } public Area3D LineOfSight { get; set; } public AnimationPlayer AnimationPlayer { get; set; } public Timer AttackTimer { get; set; } } [Meta(typeof(IAutoNode))] public partial class Enemy : CharacterBody3D, IEnemy, IProvide { public override void _Notification(int what) => this.Notify(what); public IEnemyLogic EnemyLogic { get; set; } = default!; IEnemyLogic IProvide.Value() => EnemyLogic; public EnemyLogic.IBinding EnemyBinding { get; set; } = default!; [Dependency] IGameRepo GameRepo => this.DependOn(); [Export] public EnemyStatInfo EnemyStatInfo { get; set; } = default!; public static PackedScene CollisionDetectorScene => GD.Load("res://src/enemy/CollisionDetector.tscn"); public static Area3D CollisionDetector { get; set; } = default!; public AutoProp CurrentHP { get; set; } [Node] public NavigationAgent3D NavAgent { get; set; } = default!; [Node] public Area3D LineOfSight { get; set; } = default!; [Node] public Timer PatrolTimer { get; set; } = default!; [Node] public Timer AttackTimer { get; set; } = default!; [Node] public AnimationPlayer AnimationPlayer { get; set; } = default!; [Node] public RayCast3D Raycast { get; set; } = default!; public void Setup() { EnemyLogic = new EnemyLogic(); EnemyLogic.Set(EnemyStatInfo); EnemyLogic.Set(this as IEnemy); EnemyLogic.Set(GameRepo); } public void Initialize() { CurrentHP = new AutoProp(EnemyStatInfo.MaximumHP); CurrentHP.Sync += OnHPChanged; LineOfSight.BodyEntered += LineOfSight_BodyEntered; PatrolTimer.Timeout += OnPatrolTimeout; AttackTimer.Timeout += OnAttackTimeout; var rng = new RandomNumberGenerator(); rng.Randomize(); PatrolTimer.WaitTime = rng.RandfRange(7.0f, 15.0f); } private void OnPatrolTimeout() { var rng = new RandomNumberGenerator(); rng.Randomize(); var randomizedSpot = new Vector3(rng.RandfRange(-3.0f, 3.0f), 0, rng.RandfRange(-3.0f, 3.0f)); EnemyLogic.Input(new EnemyLogic.Input.PatrolToRandomSpot(GlobalPosition + randomizedSpot)); PatrolTimer.WaitTime = rng.RandfRange(7.0f, 15.0f); } private void OnAttackTimeout() { if (GlobalPosition.DistanceTo(GameRepo.PlayerGlobalPosition.Value) > 2.5f) return; var rng = new RandomNumberGenerator(); rng.Randomize(); EnemyLogic.Input(new EnemyLogic.Input.AttackTimer()); AttackTimer.WaitTime = rng.RandfRange(2f, 3.0f); } private void LineOfSight_BodyEntered(Node3D body) { var overlappingBodies = LineOfSight.GetOverlappingBodies(); foreach (var overlap in overlappingBodies) { Raycast.LookAt(GameRepo.PlayerGlobalPosition.Value, Vector3.Up); Raycast.ForceRaycastUpdate(); if (Raycast.IsColliding()) { var collider = Raycast.GetCollider(); if (collider is IPlayer player) { Raycast.DebugShapeCustomColor = Color.FromString("Purple", Colors.Purple); EnemyLogic.Input(new EnemyLogic.Input.Alerted()); } } } } public void OnResolved() { EnemyBinding = EnemyLogic.Bind(); EnemyBinding .Handle((in EnemyLogic.Output.MovementComputed output) => { var spriteNode = GetChildren().OfType(); if (spriteNode.Any()) PlayMovementAnimations(spriteNode.Single(), Velocity); MoveAndCollide(output.Velocity); }) .Handle((in EnemyLogic.Output.Die output) => { QueueFree(); }) .Handle((in EnemyLogic.Output.HitByPlayer output) => { }); this.Provide(); EnemyLogic.Start(); } public void PlayMovementAnimations(AnimatedSprite3D sprite, Vector3 velocity) { if (sprite != null && velocity.Length() > 0.2f) { var lookdir = (GlobalPosition).Normalized(); var sign = lookdir.Sign(); if (lookdir.MaxAxisIndex() == Vector3.Axis.X && sign.X == 1) sprite.Play("walk_right"); if (lookdir.MaxAxisIndex() == Vector3.Axis.X && sign.X == -1) sprite.Play("walk_left"); if (lookdir.MaxAxisIndex() == Vector3.Axis.Z && sign.Z == 1) sprite.Play("walk_forward"); if (lookdir.MaxAxisIndex() == Vector3.Axis.Z && sign.Z == -1) sprite.Play("walk_backward"); } if (sprite != null && velocity.IsZeroApprox()) sprite.Stop(); } public void OnPhysicsProcess(double delta) { EnemyLogic.Input(new EnemyLogic.Input.PhysicsTick(delta)); } public void OnPlayerHitboxEntered(Area3D body) { if (body is IHitbox hitBox) { if (CurrentHP.Value > 0) { var damage = DamageCalculator.CalculatePlayerDamage(hitBox.Damage, hitBox.GetParent().PlayerStatInfo, EnemyStatInfo, GameRepo.EquippedWeapon); GD.Print($"Enemy Hit for {damage} damage."); EnemyLogic.Input(new EnemyLogic.Input.HitByPlayer(damage)); } } } private void OnHPChanged(double newHP) { if (newHP <= 0) EnemyLogic.Input(new EnemyLogic.Input.Killed()); } public void OnReady() { SetPhysicsProcess(true); CollisionDetector = CollisionDetectorScene.Instantiate(); CollisionDetector.AreaEntered += OnPlayerHitboxEntered; AddChild(CollisionDetector); } public void OnExitTree() { EnemyLogic.Stop(); EnemyBinding.Dispose(); } }