Cracking Open Combat

Discuss modding questions and implementation details.
Post Reply
l3lessed
Posts: 1400
Joined: Mon Aug 12, 2019 4:32 pm
Contact:

Re: Cracking Open Combat

Post by l3lessed »

Yeah, I can provide an explanation of that code and how stealth checks work based off of it. Give me a little time, as I'm busy with number of work and personal projects, so pretty busy schedule.
My Daggerfall Mod Github: l3lessed DFU Mod Github

My Beth Mods: l3lessed Nexus Page

Daggerfall Unity mods: Combat Overhaul Mod

Enjoy the free work I'm doing? Consider lending your support.

User avatar
mikeprichard
Posts: 1037
Joined: Sun Feb 19, 2017 6:49 pm

Re: Cracking Open Combat

Post by mikeprichard »

l3lessed wrote: Tue Dec 10, 2019 11:09 pm Yeah, I can provide an explanation of that code and how stealth checks work based off of it. Give me a little time, as I'm busy with number of work and personal projects, so pretty busy schedule.
Great! The primary question I'd like to resolve for classic UESP wiki purposes is whether the Stealth skill is completely "deactivated" when the player is running, but I'm pleased you ended up digging into these mechanics. Will look forward to more whenever you have the time - thanks for your hard work and many contributions to DFU modding.

l3lessed
Posts: 1400
Joined: Mon Aug 12, 2019 4:32 pm
Contact:

Re: Cracking Open Combat

Post by l3lessed »

If you wouldn't mind giving me a little shout out/credit in the wiki's you're creating/modifying using all this info I'm sharing and explaining. I would really appreciate it. :D

Short answer is yes. You cannot run and stay hidden.

However, there is a small caveat they built in; there is a time switch within the movement modifier for stealth detection. If you run, and manage to somehow stop before the next stealth check initiates, you can seem to keep the enemy in whatever detection mode they're in, and you remain stealthed. So, if the enemy doesn't even know your there, you can move and stay hidden technically. And, even if the enemy is looking for you, you can still move and stay stealthed. You just have to do it before the next stealth check happens or you're automatically found and revealed.

Now, you ask, how often does it stealth check. It stealth checks every time the classic DFU game time updates, not every cpu cycle/game frame. So, if the original time tracker for DF updated every 1 second, that is how often a stealth check is done. If it is updated every minute, that is when it is done, and so on. I don't know how often the original DF time world tracker updates, maybe hazel or someone who has messed with it can answer this part?

Another side caveat about how stealth works, the enemies have multiple states for how to handle it. There is a general detected state, but not actually found. This actives when the enemy hears or sees you. Once activated, enemy will go to the position of the last detection and try to find the hidden target, and they will stay actively in detection mode for 8 hours after detecting an enemy in their area; this is reset with every new detection too. However, players have to always be detected before a stealth check can ever happen.

Here is the longer code by code breakdown of how stealth checks work.

So, this is how the stealth check routine works from beginning to end.

First, checks if player enters or exits an area, and if there is any hostiles moving about. If not, kick out of the routine with a failed stealth check for whatever enemy is doing the stealth check.

Code: Select all

if (GameManager.Instance.PlayerEnterExit.IsPlayerInsideDungeonCastle && !motor.IsHostile)
	return false;
Checks if the thing doing the stealth check would of been spawned by classic DFU. If it wouldn't be, it returns a false check. I imagine this is some basic debug/error catching if then. Not sure if it was in the original or not though. Could of been added after to help with compatibility in mods and future features.

Code: Select all

if (!wouldBeSpawnedInClassic)
	return false;
Next, checks if the thing is within a certain distance of you, 1024 game units. If not, kick out with failed stealth check.

Code: Select all

if (distanceToTarget > 1024 * MeshReader.GlobalScale)
	return false;
Now is where it gets interesting in the code. Now, it starts using classic game time mechanics to figure out if an enemy should be in a heightened/detection state looking for the target. If the the game time hasn't updated, then the routine kicks out the current detection mode and exits the loop. Think of this as the switch that locks the enemy into the current detection mode it is in.

Code: Select all

uint gameMinutes = DaggerfallUnity.Instance.WorldTime.DaggerfallDateTime.ToClassicDaggerfallTime();
if (gameMinutes == timeOfLastStealthCheck)
	return detectedTarget;
Now, this is where the stealth movement modifier kicks in since the enemy knows something is off and is more aware to search for things and listen better. What the if then switch is saying here is, if the player runs, and doesn't stop before the next stealth check, then he is automatically found. If he stops before the next stealth check, the enemy will stay in detection mode.

Code: Select all

if (target == player)
        {
         PlayerMotor playerMotor = GameManager.Instance.PlayerMotor;
         if (playerMotor.IsMovingLessThanHalfSpeed)
         {
          	if ((gameMinutes & 1) == 1)
                        return detectedTarget;
          	}
                else if (hasEncounteredPlayer)
                    return true;
          }
                   
At this point, the code finishes by figuring out if the last time a check was done was the same time as the current game time. If it was, it ignores it. If it wasn't, it would tally all things that would affect your stealth skill and then run a stealth check to see if the player is detected by the thing looking for him.

Code: Select all

timeOfLastStealthCheck = gameMinutes;

int stealthChance = 2 * ((int)(distanceToTarget / MeshReader.GlobalScale) * target.Entity.Skills.GetLiveSkillValue(DFCareer.Skills.Stealth) >> 10);

return Dice100.FailedRoll(stealthChance);
then it rinses and repeats over and over, until the stealth check isn't needed anymore.

Code: Select all

 public bool StealthCheck()
        {
            if (GameManager.Instance.PlayerEnterExit.IsPlayerInsideDungeonCastle && !motor.IsHostile)
                return false;

            if (!wouldBeSpawnedInClassic)
                return false;

            if (distanceToTarget > 1024 * MeshReader.GlobalScale)
                return false;

            uint gameMinutes = DaggerfallUnity.Instance.WorldTime.DaggerfallDateTime.ToClassicDaggerfallTime();
            if (gameMinutes == timeOfLastStealthCheck)
                return detectedTarget;

            if (target == player)
            {
                PlayerMotor playerMotor = GameManager.Instance.PlayerMotor;
                if (playerMotor.IsMovingLessThanHalfSpeed)
                {
                    if ((gameMinutes & 1) == 1)
                        return detectedTarget;
                }
                else if (hasEncounteredPlayer)
                    return true;

                PlayerEntity player = GameManager.Instance.PlayerEntity;
                if (player.TimeOfLastStealthCheck != gameMinutes)
                {
                    player.TallySkill(DFCareer.Skills.Stealth, 1);
                    player.TimeOfLastStealthCheck = gameMinutes;
                }
            }

            timeOfLastStealthCheck = gameMinutes;

            int stealthChance = 2 * ((int)(distanceToTarget / MeshReader.GlobalScale) * target.Entity.Skills.GetLiveSkillValue(DFCareer.Skills.Stealth) >> 10);

            return Dice100.FailedRoll(stealthChance);
        }
My Daggerfall Mod Github: l3lessed DFU Mod Github

My Beth Mods: l3lessed Nexus Page

Daggerfall Unity mods: Combat Overhaul Mod

Enjoy the free work I'm doing? Consider lending your support.

l3lessed
Posts: 1400
Joined: Mon Aug 12, 2019 4:32 pm
Contact:

Re: Cracking Open Combat

Post by l3lessed »

Just wanted to point out, I updated the above post again to clarify some confusing language/code break down.
My Daggerfall Mod Github: l3lessed DFU Mod Github

My Beth Mods: l3lessed Nexus Page

Daggerfall Unity mods: Combat Overhaul Mod

Enjoy the free work I'm doing? Consider lending your support.

User avatar
mikeprichard
Posts: 1037
Joined: Sun Feb 19, 2017 6:49 pm

Re: Cracking Open Combat

Post by mikeprichard »

(edits/corrections made - see later post below)
Last edited by mikeprichard on Wed Dec 11, 2019 6:42 pm, edited 1 time in total.

Jeoshua
Posts: 153
Joined: Tue Nov 26, 2019 7:25 am

Re: Cracking Open Combat

Post by Jeoshua »

After seeing that code, I'm asking... Begging that the whole act of checking be exposed to modding and able to be replaced wholesale. I know, that's how Classic does it but I came up with ideas on how to make it better (LOS check, Hearing check, Light level check, etc) before I was even done reading it. The game doesn't need to be exactly like Thief The Dark Project but it could really be a lot better. This is ripe for modding.

l3lessed
Posts: 1400
Joined: Mon Aug 12, 2019 4:32 pm
Contact:

Re: Cracking Open Combat

Post by l3lessed »

Technically, yes, this script file could be opened up to public methods and properties. However, it couldn't easily be replaced wholesale. There are way to many dependent scripts and other game components to just replace the enemysense.cs script routines and expect the game engine to just figure out how to run your custom codes and routines.

That being said, if enemysenses.cs script was opened up to manipulation, modders could still do a ton of things to improve enemy ai in a number of areas, including stealth.
My Daggerfall Mod Github: l3lessed DFU Mod Github

My Beth Mods: l3lessed Nexus Page

Daggerfall Unity mods: Combat Overhaul Mod

Enjoy the free work I'm doing? Consider lending your support.

User avatar
mikeprichard
Posts: 1037
Joined: Sun Feb 19, 2017 6:49 pm

Re: Cracking Open Combat

Post by mikeprichard »

(Edited an earlier post:)
l3lessed wrote: Wed Dec 11, 2019 1:20 am If you wouldn't mind giving me a little shout out/credit in the wiki's you're creating/modifying using all this info I'm sharing and explaining. I would really appreciate it. :D

Short answer is yes. You cannot run and stay hidden. (...)
Wow, this is great stuff (though I admit I only understand about 75% of it, and it's definitely not because of problems with your explanation, but problems with my intellect)! I always link to the information source (and in this case will make sure to name you next to the link) whenever I update the wiki in the page update notes, so you will be credited for this. For purposes of the wiki, which tends to favor simple general explanations, I likely won't get into this level of detail, but it will be available for people to jump to and read here if they'd like more. That said, it sounds like it will be accurate for me to state the following on the wiki (building on existing language), unless you disagree. In particular, I'm not sure in the below tentative text whether a) "ambient light" actually has any involvement in the check, or b) crouching/sneaking will increase your chance of avoiding the creature finding you compared to walking, but all three (crouching/sneaking/walking) will still at least allow a chance of success - i.e. it's only running that essentially makes a successful evasion virtually impossible.

PROPOSED LANGUAGE FOR CLASSIC DAGGERFALL UESP WIKI "STEALTH" PAGE (SEE ABOVE FOR POSSIBLE ERRORS)
Whenever you approach a creature and enter its detection range, your Stealth skill is automatically checked to determine whether the creature notices you. In addition to your Stealth skill value, other factors affect your chance of successfully avoiding the creature's notice, including the creature's Stealth skill, the creature's distance from you, the ambient light, and your movement speed (moving more slowly - i.e. by crouching or sneaking instead of walking - increases your chance of successful Stealth skill checks, while running effectively results in failed Stealth skill checks).

Now, though I may be pushing my luck, for bonus karma and wiki credit (with which you won't be able to buy a cup of coffee), there is one more entirely different discrete question I've had on the wiki update backlog for a while; no worries if you haven't/wont get to this, but I appreciate your consideration regardless:

QUESTION RE: SHIELD ARMOR RATING MECHANICS
Do shields only add the displayed 1, 2, 3, or 4 armor rating to the affected body areas in the actual calculations for the player to be hit, or, similar to other armor pieces due to the fact that the displayed ratings are only 1/5 of the values actually used in these calculations, do they in fact add 5, 10, 15, or 20 in these calculations?
(See also viewtopic.php?f=4&t=1662&start=20#p19115.)

l3lessed
Posts: 1400
Joined: Mon Aug 12, 2019 4:32 pm
Contact:

Re: Cracking Open Combat

Post by l3lessed »

I made a mistake on understanding the stealth code flow, and it led me to misunderstand how this is working some.

Code: Select all

            
            if (target == player)
            {
                PlayerMotor playerMotor = GameManager.Instance.PlayerMotor;
                if (playerMotor.IsMovingLessThanHalfSpeed)
                {
                    if ((gameMinutes & 1) == 1)
                        return detectedTarget;
                }
                else if (hasEncounteredPlayer)
                    return true;

                PlayerEntity player = GameManager.Instance.PlayerEntity;
                if (player.TimeOfLastStealthCheck != gameMinutes)
                {
                    player.TallySkill(DFCareer.Skills.Stealth, 1);
                    player.TimeOfLastStealthCheck = gameMinutes;
                }
            }
So, in order to truly understand stealth mechanics, you need to look at the beginning update routine, as that is what actually controls the enemies senses, including stealth mechanics. The stealth check merely is a if then switch that moves the enemy into a new ai mode if it is triggered. I honestly don't feel like doing a code break down of that routine, as it is kind of long. But I'll provide a quick summary.

First, is ensures the enemy is spawned within the classic game engine rules and then switches the spawned classic trigger to true, so I was right about it being an error catcher.

Second, it checks for a classic game update interval to maintain classic stealth mechanic update times and operations. If update happens, starts a senses update routine to check the enemies sense and tell them what to do.

Checks if enemy is hostile or not. If not, makes enemy have no target and then resets all the needed codes and vars. Also, checks for any other targets the enemy may have saved and be looking for and clears them.

Next, a target is found, it updates where the enemy is going and how fast they will go there so they now move towards the player/target.

Now, it begins the routine for seeing and hearing senses. It first just sees of the enemy has anyone within site of him. If so, it will set them as a target and set their position so the enemy can head that way in search of his target.

Next, if the enemy can't see you, it does a hearing check to see if the enemy can hear you in the area.

At this point, if the enemy can either hear or see you, it updates where the enemy is headed to the new location is think it heard or saw you, and then resets the last time it had registered hearing or seeing you. This is also where it triggers the hasEncounterPlayer trigger, so the enemy will stay in a heightened aware state, even if you run and hide again.

Now, this is where the stealth check actually happens within the enemies brain/code flow. Now it has detected a target in the area and has a location to head to, it will start doing stealth checks every update cycle, and after every stealth check, it will also update your position automatically; this is incase you are heard but not seen, the enemy knows the new location to search for you at. The moment you are seen or heard you come out of hiding; however, the moment you get out of site and are not heard anymore, you go back into stealth mode and stealth checks begin. However, at this point, you cannot move faster than base stealth speed without automatically being found.

At this point, either the stealth check works, and enemy keeps looking for you using his hearing and site senses that are continually updating in his code flow/ai brain, or it fails, you're detected, and combat normal play starts. This again is affect by if the enemy has seen or heard you (AKA has Encountered Player) or not.

Now to this code:

Code: Select all

            
            if (target == player)
            {
                PlayerMotor playerMotor = GameManager.Instance.PlayerMotor;
                if (playerMotor.IsMovingLessThanHalfSpeed)
                {
                    if ((gameMinutes & 1) == 1)
                        return detectedTarget;
                }
                else if (hasEncounteredPlayer)
                    return true;

                PlayerEntity player = GameManager.Instance.PlayerEntity;
                if (player.TimeOfLastStealthCheck != gameMinutes)
                {
                    player.TallySkill(DFCareer.Skills.Stealth, 1);
                    player.TimeOfLastStealthCheck = gameMinutes;
                }
            }
Small clarifications. If you move more than half your base speed, it will kick into the hasEncounterPlayer trigger. If not, the enemy stays in current detection state. HasEncounterPlayer trigger is used to move the enemy out of the detection stage and into combat stage. As long as the stealth checks continue successfully, the detectedtarget trigger continually gets reset to false before the enemy senses code flow can finish detecting you.

So here is a flow chart of some type:
Checks player is in detectable range -> checks if enemy can hear or see target/player -> True -> checks to see if enemy is pacified -> True -> Enemy non-hostile.

Checks player is in detectable range -> checks if enemy can hear or see target/player -> false -> checks to see if you succeed stealth check -> True -> Enemy stays in detection mode looking for you using last sensed location until it sees or hears you again.

One thing is, if you haven't been encountered yet, and you move faster than base sneak speed, you can still sneak and have stealth checks happen. That is where I think confusion is happening for players. Once you are encountered by an enemy, you can no longer move while stealthed without automatically being detected on next update. However, until the enemy sees or hears you and registers an encounter (AKA the enemy code flow/brain registers there is something in the area), you can try to move faster than base sneak speed and have stealth checks work. So, it depends on if the enemy already knows you are an actual thing it has seen or heard or whether it has no awareness of you yet.

If it has awareness of you, you can't move faster than base sneak speed without automatically being found.

if enemy has no awareness of you yet, you can try to move faster then base sneak speed and stealth checks and mechanics work.

the gameMinutes trigger I think is just a trigger to ensure the script runs on classic DF time intervals, and nothing more.

Makes sense from a ai perspective. If something has no awareness, it is easier to move around them. If it knows there is something off, it is very hard to move around quickly without being heard or seen.
My Daggerfall Mod Github: l3lessed DFU Mod Github

My Beth Mods: l3lessed Nexus Page

Daggerfall Unity mods: Combat Overhaul Mod

Enjoy the free work I'm doing? Consider lending your support.

l3lessed
Posts: 1400
Joined: Mon Aug 12, 2019 4:32 pm
Contact:

Re: Cracking Open Combat

Post by l3lessed »

As for the shield, I need little time to breakdown the logic of the below code but here is the block of code the controls what you're asking about. The shield is handled completely in a armor value update routine that is called anytime a player needs their armor values updated.

Code: Select all

		    // Shield armor values in classic are unaffected by their material type.
                    int[] values = { 0, 0, 0, 0, 0, 0, 0 }; // shield's effect on the 7 armor values
                    int armorBonus = armor.GetShieldArmorValue();
                    BodyParts[] protectedBodyParts = armor.GetShieldProtectedBodyParts();

                    foreach (var BodyPart in protectedBodyParts)
                    {
                        values[(int)BodyPart] = armorBonus;
                    }

                    for (int i = 0; i < armorValues.Length; i++)
                    {
                        if (equipping)
                        {
                            armorValues[i] -= (sbyte)(values[i] * 5);
                        }
                        else
                        {
                            armorValues[i] += (sbyte)(values[i] * 5);
                        }
My Daggerfall Mod Github: l3lessed DFU Mod Github

My Beth Mods: l3lessed Nexus Page

Daggerfall Unity mods: Combat Overhaul Mod

Enjoy the free work I'm doing? Consider lending your support.

Post Reply