[0.10.24] Enemy NPCs At Very Low Speed Attribute Don't Attack

Curated user bug reports with enough information to reproduce problem. Moderators will move topics here from Help & Support once a bug has been confirmed and enough information has been provided.
Post Reply
User avatar
Magicono43
Posts: 508
Joined: Tue Nov 06, 2018 7:06 am

[0.10.24] Enemy NPCs At Very Low Speed Attribute Don't Attack

Post by Magicono43 »

Describe the bug
When an enemy NPC has their speed attribute reduced enough, usually with a few applications of a drain speed spell. They will no longer attack. They will still be hostile and actively pursuing the player, or other targets, but they seem to no longer be able to attack in melee at all. If you hit them with a heal speed spell, they will go back to attacking like normal again, but if reduced below some point, they will be unable to attack once again.

To Reproduce
- Make a drain speed spell, make it whatever effect, on touch usually works well.
- Go up to an enemy NPC that is hostile toward you and start spamming them with the drain speed effect.
- You will know it worked once they stop attacking all together, but are still actively pursuing you.
- You can lead them all around and have a staring contest, but they will not be able to attack in melee.

- If you wish, you can do an on touch "heal speed" spell on them, to confirm once their speed state is brought back up, they can once again attack.
- This may be somewhat difficult to reproduce because of this other mentioned bug: viewtopic.php?f=5&t=3912

Expected behavior
I never used the drain spell before in classic Daggerfall, but I assume the intended behavior would be for the enemy with the drained speed to still be able to attack, but only at an extremely slow rate, as is the case with the player having massively increased animation times for their melee attacks.

Screenshots
No screenshots as it this can be fairly easily reproduced with console commands. If needed I can add some.

Desktop (please complete the following information):
OS: Windows
Version: Alpha 0.10.24
Additional context

I have a hunch that the cause of this may be from this block of code in the "EnemyAttack.cs" script, but i'm not 100% sure. It either seems like a bug with the AI, or the enemy is literally not being allowed to have their MeleeAnimation timer reset with their speed at such low levels.:

EnemyAttack.cs

Code: Select all

void FixedUpdate()
        {
            // Unable to attack if AI disabled or paralyzed
            if (GameManager.Instance.DisableAI || entityBehaviour.Entity.IsParalyzed)
                return;

            // Unable to attack when playing certain oneshot anims
            if (mobile && mobile.IsPlayingOneShot() && mobile.OneShotPauseActionsWhilePlaying())
                return;

            // Countdown to next melee attack
            MeleeTimer -= Time.deltaTime;

            if (MeleeTimer < 0)
                MeleeTimer = 0;

            EnemyEntity entity = entityBehaviour.Entity as EnemyEntity;
            int speed = entity.Stats.LiveSpeed;

            // Note: Speed comparison here is reversed from classic. Classic's way makes fewer attack
            // attempts at higher speeds, so it seems backwards.
            if (GameManager.ClassicUpdate && (DFRandom.rand() % speed >= (speed >> 3) + 6 && MeleeTimer == 0))
            {
                if (!MeleeAnimation())
                    return;

                ResetMeleeTimer();
            }
        }
Here is a save-file as well for even easier reproduction of steps, if needed:
SAVE354.rar
(185.87 KiB) Downloaded 24 times

User avatar
Interkarma
Posts: 5831
Joined: Sun Mar 22, 2015 1:51 am

Re: [0.10.24] Enemy NPCs At Very Low Speed Attribute Don't Attack

Post by Interkarma »

The ClassicUpdate timer and this attack rate formula was written by Allofich. His work is usually accurate in reverse engineering and fixing classic bugs, even when the outcome is unexpected.

I'll move to bug reports for now and investigate later. If someone has a chance to compare in classic before then and report back, that would be appreciated.

User avatar
Magicono43
Posts: 508
Joined: Tue Nov 06, 2018 7:06 am

Re: [0.10.24] Enemy NPCs At Very Low Speed Attribute Don't Attack

Post by Magicono43 »

Interkarma wrote: Mon Jul 13, 2020 7:30 pm The ClassicUpdate timer and this attack rate formula was written by Allofich. His work is usually accurate in reverse engineering and fixing classic bugs, even when the outcome is unexpected.

I'll move to bug reports for now and investigate later. If someone has a chance to compare in classic before then and report back, that would be appreciated.
So I just did a little testing in DOS and found that (as Allofich notes) that the drain speed effect actually does the opposite to enemies it is used on, they get super fast and frequent attack animations. But unfortunately it does not seem to work the opposite way for "heal speed". So unless some hex editing is done, not sure if I know how to properly test this feature in DOS Daggerfall.

User avatar
Magicono43
Posts: 508
Joined: Tue Nov 06, 2018 7:06 am

Re: [0.10.24] Enemy NPCs At Very Low Speed Attribute Don't Attack

Post by Magicono43 »

So I was playing around with the EnemyAttack.cs script, specifically the "ResetMeleeTimer" method, and the "FixedUpdate" method that it is called from. I found the cause of the enemies not attacking if their speed stat gets low enough, it's this if-statement parameter in FixedUpdate:

Code: Select all

if (GameManager.ClassicUpdate && (DFRandom.rand() % speed >= (speed >> 3) + 6 && MeleeTimer == 0))
specifically the middle parameter: (DFRandom.rand() % speed >= (speed >> 3) + 6
When the speed stat of an NPC entity goes to 6 or below, they no longer are able to use melee attacks, which may also include touch-spell potentially.

I can only assume this parameter is here as a way to simulate the increase or decrease of the speed stat and to in theory make enemies attack more or less frequently based on their current speed stat. However, I have noticed that the effect is very minor, if at all, due to this being checked and rerolled essentially every frame, making the lower speed scores not seem different to the high-speed scores at all, and eventually causing the previously mentioned issue of disallowing all attacks at 6 or below speed.


For my own curiosity and possibly your input on this as well, I slightly altered these two methods in EnemyAttack.cs to see the result, and the effect is much more pronounced, and also does not have that quirk of disallowing attacks after a certain threshold.


Here are the changes I made and did some testing with:
From this:

Code: Select all

void FixedUpdate()
        {
            // Unable to attack if AI disabled or paralyzed
            if (GameManager.Instance.DisableAI || entityBehaviour.Entity.IsParalyzed)
                return;

            // Unable to attack when playing certain oneshot anims
            if (mobile && mobile.IsPlayingOneShot() && mobile.OneShotPauseActionsWhilePlaying())
                return;

            // Countdown to next melee attack
            MeleeTimer -= Time.deltaTime;

            if (MeleeTimer < 0)
                MeleeTimer = 0;

            EnemyEntity entity = entityBehaviour.Entity as EnemyEntity;
            int speed = entity.Stats.LiveSpeed;

            // Note: Speed comparison here is reversed from classic. Classic's way makes fewer attack
            // attempts at higher speeds, so it seems backwards.
            if (GameManager.ClassicUpdate && (DFRandom.rand() % speed >= (speed >> 3) + 6 && MeleeTimer == 0))
            {
                if (!MeleeAnimation())
                    return;

                ResetMeleeTimer();
            }
        }
To this:

Code: Select all

void FixedUpdate()
        {
            // Unable to attack if AI disabled or paralyzed
            if (GameManager.Instance.DisableAI || entityBehaviour.Entity.IsParalyzed)
                return;

            // Unable to attack when playing certain oneshot anims
            if (mobile && mobile.IsPlayingOneShot() && mobile.OneShotPauseActionsWhilePlaying())
                return;

            // Countdown to next melee attack
            MeleeTimer -= Time.deltaTime;

            if (MeleeTimer < 0)
                MeleeTimer = 0;

            // Note: Speed comparison here is reversed from classic. Classic's way makes fewer attack
            // attempts at higher speeds, so it seems backwards.
            if (GameManager.ClassicUpdate && MeleeTimer == 0)
            {
                if (!MeleeAnimation())
                    return;

                ResetMeleeTimer();
            }
        }

And from this under the ResetMeleeTimer method:

Code: Select all

public void ResetMeleeTimer()
        {
            MeleeTimer = Random.Range(1500, 3000 + 1);
            MeleeTimer -= 50 * (GameManager.Instance.PlayerEntity.Level - 10);

            // Note: In classic, what happens here is
            // meleeTimer += 450 * (enemydata[130] - 2);
            // Looks like this was meant to reference the game reflexes setting,
            // which is stored in playerentitydata[130].
            // Instead enemydata[130] seems to instead always be 0, the equivalent of
            // "very high" reflexes, regardless of what the game reflexes are.
            // Here, we use the reflexes data as was intended.
            MeleeTimer += 450 * ((int)GameManager.Instance.PlayerEntity.Reflexes - 2);

            if (MeleeTimer < 0)
                MeleeTimer = 0;

            MeleeTimer /= 980; // Approximates classic frame update
        }
To this, which adds or subtracts a "fixed" MeleeTimer value onto the final amount based on the entity's current speed:

Code: Select all

public void ResetMeleeTimer()
        {
            EnemyEntity entity = entityBehaviour.Entity as EnemyEntity;
            int speed = (entity.Stats.LiveSpeed - 70) * -1;
            float speedMeleeTimerMod = (speed * .03f) * 1000;

            MeleeTimer = Random.Range(2000, 4000 + 1);
            MeleeTimer -= 50 * (GameManager.Instance.PlayerEntity.Level - 10);
            MeleeTimer += speedMeleeTimerMod;

            // Note: In classic, what happens here is
            // meleeTimer += 450 * (enemydata[130] - 2);
            // Looks like this was meant to reference the game reflexes setting,
            // which is stored in playerentitydata[130].
            // Instead enemydata[130] seems to instead always be 0, the equivalent of
            // "very high" reflexes, regardless of what the game reflexes are.
            // Here, we use the reflexes data as was intended.
            MeleeTimer += 450 * ((int)GameManager.Instance.PlayerEntity.Reflexes - 2);

            if (MeleeTimer < 0)
                MeleeTimer = 0;

            MeleeTimer /= 980; // Approximates classic frame update
        }
I made it -70 instead of -50, since most enemies in the game are around the average of 70 speed to begin with, and it slightly exaggerates the effect of a monster with a very low speed, unlike before these changes.

From that small amount of testing in Classic DF that I did, I was able to see that the speed stat does increase attack rate pretty significantly, but was unable to test the opposite with lower speed, but I would have to assume that it was intended to work the other way around as well.

If you find this alteration appropriate, i'll make a PR for it, but that's up to you of course if you like this "solution" or not. :)

User avatar
Interkarma
Posts: 5831
Joined: Sun Mar 22, 2015 1:51 am

Re: [0.10.24] Enemy NPCs At Very Low Speed Attribute Don't Attack

Post by Interkarma »

Thanks for the feedback, much appreciated! I can see the issue, and I'd like to take a different approach to resolve. No need to send a PR for this one. :)

User avatar
Magicono43
Posts: 508
Joined: Tue Nov 06, 2018 7:06 am

Re: [0.10.24] Enemy NPCs At Very Low Speed Attribute Don't Attack

Post by Magicono43 »

Interkarma wrote: Wed Jul 15, 2020 12:27 am Thanks for the feedback, much appreciated! I can see the issue, and I'd like to take a different approach to resolve. No need to send a PR for this one. :)
Roger that, i'm curious to see what you think up. My fancy math skills are pretty limited as you can see, lol, most of my changes are completely linear in function.

Post Reply