summaryrefslogtreecommitdiff
path: root/scripts/Enemy.cs
blob: d2f64770acd49c41744b06ce48d149d3d9e24666 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
using Godot;
using testing.interfaces; // Match your namespace

public partial class Enemy : CharacterBody2D, IDamageable
{
    [Export] public float MoveSpeed { get; set; } = 150.0f;
    [Export] public float MaxHealth { get; set; } = 30.0f;
    [Export] public float Damage { get; set; } = 50.0f;
    [Export] public float AttackRange = 40.0f;    // Distance to trigger swing
    [Export] public float AttackCooldown = 1.2f;
    [Export] public float SteeringAcceleration { get; set; } = 12.0f; // Higher = tighter turns, lower = smoother turns
    [Export] public float SwingAngle = 150.0f;  // The parent node holding the weapon

    private NavigationAgent2D _nav;

    [Export] public Node2D WeaponPivot;   // The parent node holding the weapon
    [Export] public Area2D WeaponHitbox;
    private bool _isAttacking = false;
    private bool _canAttack = true;

    private Node2D _player;
    private Vector2 _lastPlayerPos;
    
    public override void _Ready()
    {
        _nav = GetNode<NavigationAgent2D>("NavigationAgent2D");
        _player = GetTree().GetFirstNodeInGroup("player") as CharacterBody2D;
        _lastPlayerPos = _player.GlobalPosition;
        Callable.From(ActorSetup).CallDeferred();

        _nav.AvoidanceEnabled = true;

        // Connect the VelocityComputed signal to receive safe avoidance vectors
        _nav.VelocityComputed += OnVelocityComputed;

        if (WeaponHitbox != null)
        {
            // Keep hitbox turned off when not swinging
            WeaponHitbox.Monitoring = false;
            WeaponHitbox.BodyEntered += OnWeaponHitboxBodyEntered;
        }

    }

    private async void ActorSetup()
    {
        await ToSignal(GetTree(), SceneTree.SignalName.PhysicsFrame);
        SetMovementTarget(_player.Position);
    }

    public void SetMovementTarget(Vector2 movementTarget)
    {
        _nav.TargetPosition = movementTarget;
    }

    public override void _PhysicsProcess(double delta)
    {
        if (!GodotObject.IsInstanceValid(_player) || _isAttacking) return;

        // Calculate direction vector pointing from the enemy to the player
        // if (GlobalPosition.DistanceSquaredTo(_lastPlayerPos) > 256.0f) 
        // {
        //     _lastPlayerPos = _player.GlobalPosition;
        //     _nav.TargetPosition = _lastPlayerPos;
        // }
        _nav.TargetPosition = _player.GlobalPosition;
        float distanceToPlayer = GlobalPosition.DistanceTo(_player.GlobalPosition);
        if (distanceToPlayer <= AttackRange)
        {
            if (_canAttack)
            {
                PerformSwingAttack();
                return;
            }
        }
        

        if (_nav.IsNavigationFinished())
        {
            _nav.Velocity = Vector2.Zero;
            Velocity = Velocity.MoveToward(Vector2.Zero, (float)delta * MoveSpeed * SteeringAcceleration);
            MoveAndSlide();
            return;
        }

        Vector2 nextPathPosition = _nav.GetNextPathPosition();
        Vector2 desiredDirection = GlobalPosition.DirectionTo(nextPathPosition);
        Vector2 targetVelocity = desiredDirection * MoveSpeed;

        WeaponPivot.Rotation = desiredDirection.Angle();

        // 5. Pass target velocity to the NavigationAgent for processing
        if (_nav.AvoidanceEnabled)
        {
            // Triggers OnVelocityComputed signal on the next frame calculation
            _nav.Velocity = targetVelocity;
        }
        else
        {
            // Fallback smooth steering without avoidance
            ApplySmoothSteering(targetVelocity, (float)delta);
        }
    }
    private async void PerformSwingAttack(){
        _isAttacking = true;
        _canAttack = false;
        Velocity = Vector2.Zero; // Stop moving while swinging

        // 1. Calculate the angle pointing toward the player
        float aimAngle = (_player.GlobalPosition - GlobalPosition).Angle();
        
        // Define swing arc (-60 degrees back to +60 degrees forward)
        float startAngle = aimAngle - Mathf.DegToRad(SwingAngle/2);
        float endAngle = aimAngle + Mathf.DegToRad(SwingAngle/2);

        // 2. Set starting position for the swing


        Tween prepTween = CreateTween();
        // 3. Create a Tween to animate the rotation
        prepTween.TweenProperty(WeaponPivot, "rotation", startAngle, 0.25f)
                  .SetTrans(Tween.TransitionType.Quad)
                  .SetEase(Tween.EaseType.Out);
        await ToSignal(prepTween, Tween.SignalName.Finished);
        WeaponPivot.Rotation = startAngle;
        WeaponHitbox.Monitoring = true; // Turn on collision check

        Tween swingTween = CreateTween();
        
        // Rotate from startAngle to endAngle over 0.25 seconds with a smooth curve
        swingTween.TweenProperty(WeaponPivot, "rotation", endAngle, 0.25f)
                  .SetTrans(Tween.TransitionType.Quad)
                  .SetEase(Tween.EaseType.Out);

        // Wait for the swing movement to complete
        await ToSignal(swingTween, Tween.SignalName.Finished);

        // 4. Disable hitbox as soon as swing finishes
        WeaponHitbox.Monitoring = false;

        // 5. Retract weapon back to neutral position smoothly
        Tween resetTween = CreateTween();
        resetTween.TweenProperty(WeaponPivot, "rotation", aimAngle, 0.15f);
        await ToSignal(resetTween, Tween.SignalName.Finished);

        _isAttacking = false;

        // Cooldown timer before the enemy can swing again
        await ToSignal(GetTree().CreateTimer(AttackCooldown), SceneTreeTimer.SignalName.Timeout);
        _canAttack = true;
    }

    private void OnWeaponHitboxBodyEntered(Node2D body)
    {
        // Deal damage when the weapon hits a damageable target
        if (body is IDamageable target)
        {
            target.TakeDamage(Damage);
        }
    }
    private void OnVelocityComputed(Vector2 safeVelocity)
    {
        Velocity = safeVelocity;
        MoveAndSlide();
    }
    private void ApplySmoothSteering(Vector2 targetVelocity, float delta)
    {
        Velocity = Velocity.Lerp(targetVelocity, delta * SteeringAcceleration);
        MoveAndSlide();
    }


    public void TakeDamage(float amount)
    {
        MaxHealth -= amount;
        GD.Print($"Enemy took {amount} damage! Health remaining: {MaxHealth}");

        if (MaxHealth <= 0)
        {
            GD.Print("Enemy defeated!");
            QueueFree(); // Removes the enemy from the game
        }
    }
}