Files
GameJamDungeon/src/enemy/Enemy.cs

207 lines
5.7 KiB
C#

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<double> 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<IEnemyLogic>
{
public override void _Notification(int what) => this.Notify(what);
public IEnemyLogic EnemyLogic { get; set; } = default!;
IEnemyLogic IProvide<IEnemyLogic>.Value() => EnemyLogic;
public EnemyLogic.IBinding EnemyBinding { get; set; } = default!;
[Dependency] IGameRepo GameRepo => this.DependOn<IGameRepo>();
[Export]
public EnemyStatInfo EnemyStatInfo { get; set; } = default!;
public static PackedScene CollisionDetectorScene => GD.Load<PackedScene>("res://src/enemy/CollisionDetector.tscn");
public static Area3D CollisionDetector { get; set; } = default!;
public AutoProp<double> 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<double>(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<AnimatedSprite3D>();
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<IPlayer>().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<Area3D>();
CollisionDetector.AreaEntered += OnPlayerHitboxEntered;
AddChild(CollisionDetector);
}
public void OnExitTree()
{
EnemyLogic.Stop();
EnemyBinding.Dispose();
}
}