How does AC of a creature works?

Discuss modding questions and implementation details.
Post Reply
Zomgmeister
Posts: 21
Joined: Mon Aug 05, 2019 3:05 pm

How does AC of a creature works?

Post by Zomgmeister » Thu Aug 22, 2019 6:08 pm

I checked out creature stats at https://en.uesp.net/wiki/Daggerfall:Bestiary. There is a stat called AC, presumably Armor Class, but I really do not understand it.

Higher is better, as for player character? But Ancient Lich has -12 and Daedra Lord has -10, while Rat and Grizzly Bear has 6.

Lower is better, as in D&D? But Orc and Orc Shaman have 7, which is worse than 6, and Orc Warlord has 16, which may be a typo.

When I tried to attack an Ancient Lich in Scourg Barrow with Mithril Dai-Katana, I could not lay a single hit, so it seems that negative values works as a powerful protection. In general, it looks like Lower is better, which is meh, but I can't be sure. Is there any formula that actually works and does use this?

User avatar
Jay_H
Posts: 2850
Joined: Tue Aug 25, 2015 1:54 am

Re: How does AC of a creature works?

Post by Jay_H » Thu Aug 22, 2019 6:17 pm

The Elder Scrolls games were originally based off D&D, so the AC does improve as it goes lower. If an Orc Warlord has that high AC, that's his problem :lol:

For the PC high armor values increase the ability to evade attacks but provide no damage reduction.
ON HIATUS. Will be difficult to find.
Come join the Unofficial Daggerfall Unity Discord.
See the Daggerfall Unity Wiki on the UESP.

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

Re: How does AC of a creature works?

Post by l3lessed » Fri Aug 23, 2019 4:42 pm

As jay said.

You may want to check our my combat threads to get a better idea of the underlying mechanics of combat.

However, I needed to understand how this operated better anyway, so I spent 30 minutes going through the coding. So, this seems to be how it sets up monsters from beginning to end. It isn't too complicated, as it repurposes the character creation script and system with minor modifications.

So, DFCareer.cs is where a players and monsters stat and attributes instances/objects are created, stored, and retrieved.
The playerEntity and EnemyEntity script files are where the actual player or enemy instances/objects are created, initialized, injected, and stored.

So, the engine is running along, creating this simple 2d fantasy reality, we perceive as the Daggerfall Game.
The player enters a dungeon, and the universe/engine has, through quantum 1's and 0's, created enemies out of thin air.
At this point, the engine goes, "What do all these floating, blinking 1's and 0's constitute when combined?"
EnemyEntity goes, "Oh, hello, I'm what you call a Lich Lord(enemy). But, one sec, I need to check specifically how strong I am,."
DFCareer goes, "Oh, hey Lich, remember, you're a bad ass, you got all these deadly spells, ancient magical equipment, and raise undead. You're going to fuck some shit up."
EnemyEntity goes, "Oh thanks for the reminder. Hey player, I'm going to fuck your day up with this fireball to your face."
and the Engine god rolls along popping quantum 1's and 0's out that constitute daggerfall.

When it comes to their specific armor value, it checks if the equipment they have is better than their enemy type's base armor. If it is, it uses the armor value instead of their base monster type armor value.

EnemyEntity.cs (Here's the code that sets up the enemy)

Code: Select all

public void SetEnemyCareer(MobileEnemy mobileEnemy, EntityTypes entityType)
        {
            if (entityType == EntityTypes.EnemyMonster)
            {
                careerIndex = mobileEnemy.ID;
                career = GetMonsterCareerTemplate((MonsterCareers)careerIndex);
                stats.SetPermanentFromCareer(career);

                // Enemy monster has predefined level, health and armor values.
                // Armor values can be modified below by equipment.
                level = mobileEnemy.Level;
                maxHealth = UnityEngine.Random.Range(mobileEnemy.MinHealth, mobileEnemy.MaxHealth + 1);
                for (int i = 0; i < ArmorValues.Length; i++)
                {
                    ArmorValues[i] = (sbyte)(mobileEnemy.ArmorValue * 5);
                }
            }
            else if (entityType == EntityTypes.EnemyClass)
            {
                careerIndex = mobileEnemy.ID - 128;
                career = GetClassCareerTemplate((ClassCareers)careerIndex);
                stats.SetPermanentFromCareer(career);

                // Enemy class is levelled to player and uses similar health rules
                // City guards are 3 to 6 levels above the player
                level = GameManager.Instance.PlayerEntity.Level;
                if (careerIndex == (int)MobileTypes.Knight_CityWatch)
                    level += UnityEngine.Random.Range(3, 7);

                maxHealth = FormulaHelper.RollEnemyClassMaxHealth(level, career.HitPointsPerLevel);
            }
            else
            {
                career = new DFCareer();
                careerIndex = -1;
                return;
            }

            this.mobileEnemy = mobileEnemy;
            this.entityType = entityType;
            name = career.Name;
            minMetalToHit = mobileEnemy.MinMetalToHit;
            team = mobileEnemy.Team;

            short skillsLevel = (short)((level * 5) + 30);
            if (skillsLevel > 100)
            {
                skillsLevel = 100;
            }

            for (int i = 0; i <= DaggerfallSkills.Count; i++)
            {
                skills.SetPermanentSkillValue(i, skillsLevel);
            }

            // Generate loot table items
            DaggerfallLoot.GenerateItems(mobileEnemy.LootTableKey, items);

            // Enemy classes and some monsters use equipment
            if (careerIndex == (int)MonsterCareers.Orc || careerIndex == (int)MonsterCareers.OrcShaman)
            {
                SetEnemyEquipment(0);
            }
            else if (careerIndex == (int)MonsterCareers.Centaur || careerIndex == (int)MonsterCareers.OrcSergeant)
            {
                SetEnemyEquipment(1);
            }
            else if (careerIndex == (int)MonsterCareers.OrcWarlord)
            {
                SetEnemyEquipment(2);
            }
            else if (entityType == EntityTypes.EnemyClass)
            {
                SetEnemyEquipment(UnityEngine.Random.Range(0, 2)); // 0 or 1
            }

            // Assign spell lists
            if (entityType == EntityTypes.EnemyMonster)
            {
                if (careerIndex == (int)MonsterCareers.Imp)
                    SetEnemySpells(ImpSpells);
                else if (careerIndex == (int)MonsterCareers.Ghost)
                    SetEnemySpells(GhostSpells);
                else if (careerIndex == (int)MonsterCareers.OrcShaman)
                    SetEnemySpells(OrcShamanSpells);
                else if (careerIndex == (int)MonsterCareers.Wraith)
                    SetEnemySpells(WraithSpells);
                else if (careerIndex == (int)MonsterCareers.FrostDaedra)
                    SetEnemySpells(FrostDaedraSpells);
                else if (careerIndex == (int)MonsterCareers.FireDaedra)
                    SetEnemySpells(FireDaedraSpells);
                else if (careerIndex == (int)MonsterCareers.Daedroth)
                    SetEnemySpells(DaedrothSpells);
                else if (careerIndex == (int)MonsterCareers.Vampire)
                    SetEnemySpells(VampireSpells);
                else if (careerIndex == (int)MonsterCareers.DaedraSeducer)
                    SetEnemySpells(SeducerSpells);
                else if (careerIndex == (int)MonsterCareers.VampireAncient)
                    SetEnemySpells(VampireAncientSpells);
                else if (careerIndex == (int)MonsterCareers.DaedraLord)
                    SetEnemySpells(DaedraLordSpells);
                else if (careerIndex == (int)MonsterCareers.Lich)
                    SetEnemySpells(LichSpells);
                else if (careerIndex == (int)MonsterCareers.AncientLich)
                    SetEnemySpells(AncientLichSpells);
            }
            else if (entityType == EntityTypes.EnemyClass && (mobileEnemy.CastsMagic))
            {
                int spellListLevel = level / 3;
                if (spellListLevel > 6)
                    spellListLevel = 6;
                SetEnemySpells(EnemyClassSpells[spellListLevel]);
            }

            // Chance of adding map
            DaggerfallLoot.RandomlyAddMap(mobileEnemy.MapChance, items);

            if (!string.IsNullOrEmpty(mobileEnemy.LootTableKey))
            {
                // Chance of adding potion
                DaggerfallLoot.RandomlyAddPotion(3, items);
                // Chance of adding potion recipe
                DaggerfallLoot.RandomlyAddPotionRecipe(2, items);
            }

            FillVitalSigns();
        }
EnemyEntity.cs(Here's were the enemy equipment and this armor is setup, if they don't have a negative monster armor value.)

Code: Select all

 public void SetEnemyEquipment(int variant)
        {
            PlayerEntity player = GameManager.Instance.PlayerEntity;
            int itemLevel = player.Level;
            Genders playerGender = player.Gender;
            Races race = player.Race;
            int chance = 0;

            // City watch never have items above iron or steel
            if (entityType == EntityTypes.EnemyClass && mobileEnemy.ID == (int)MobileTypes.Knight_CityWatch)
                itemLevel = 1;

            if (variant == 0)
            {
                // right-hand weapon
                int item = UnityEngine.Random.Range((int)Game.Items.Weapons.Broadsword, (int)(Game.Items.Weapons.Longsword) + 1);
                Items.DaggerfallUnityItem weapon = Game.Items.ItemBuilder.CreateWeapon((Items.Weapons)item, Game.Items.ItemBuilder.RandomMaterial(itemLevel));
                ItemEquipTable.EquipItem(weapon, true, false);
                items.AddItem(weapon);

                chance = 50;

                // left-hand shield
                item = UnityEngine.Random.Range((int)Game.Items.Armor.Buckler, (int)(Game.Items.Armor.Round_Shield) + 1);
                if (Dice100.SuccessRoll(chance))
                {
                    Items.DaggerfallUnityItem armor = Game.Items.ItemBuilder.CreateArmor(playerGender, race, (Items.Armor)item, Game.Items.ItemBuilder.RandomArmorMaterial(itemLevel));
                    ItemEquipTable.EquipItem(armor, true, false);
                    items.AddItem(armor);
                }
                // left-hand weapon
                else if (Dice100.SuccessRoll(chance))
                {
                    item = UnityEngine.Random.Range((int)Game.Items.Weapons.Dagger, (int)(Game.Items.Weapons.Shortsword) + 1);
                    weapon = Game.Items.ItemBuilder.CreateWeapon((Items.Weapons)item, Game.Items.ItemBuilder.RandomMaterial(itemLevel));
                    ItemEquipTable.EquipItem(weapon, true, false);
                    items.AddItem(weapon);
                }
            }
            else
            {
                // right-hand weapon
                int item = UnityEngine.Random.Range((int)Game.Items.Weapons.Claymore, (int)(Game.Items.Weapons.Battle_Axe) + 1);
                Items.DaggerfallUnityItem weapon = Game.Items.ItemBuilder.CreateWeapon((Items.Weapons)item, Game.Items.ItemBuilder.RandomMaterial(itemLevel));
                ItemEquipTable.EquipItem(weapon, true, false);
                items.AddItem(weapon);

                if (variant == 1)
                    chance = 75;
                else if (variant == 2)
                    chance = 90;
            }
            // helm
            if (Dice100.SuccessRoll(chance))
            {
                Items.DaggerfallUnityItem armor = Game.Items.ItemBuilder.CreateArmor(playerGender, race, Game.Items.Armor.Helm, Game.Items.ItemBuilder.RandomArmorMaterial(itemLevel));
                ItemEquipTable.EquipItem(armor, true, false);
                items.AddItem(armor);
            }
            // right pauldron
            if (Dice100.SuccessRoll(chance))
            {
                Items.DaggerfallUnityItem armor = Game.Items.ItemBuilder.CreateArmor(playerGender, race, Game.Items.Armor.Right_Pauldron, Game.Items.ItemBuilder.RandomArmorMaterial(itemLevel));
                ItemEquipTable.EquipItem(armor, true, false);
                items.AddItem(armor);
            }
            // left pauldron
            if (Dice100.SuccessRoll(chance))
            {
                Items.DaggerfallUnityItem armor = Game.Items.ItemBuilder.CreateArmor(playerGender, race, Game.Items.Armor.Left_Pauldron, Game.Items.ItemBuilder.RandomArmorMaterial(itemLevel));
                ItemEquipTable.EquipItem(armor, true, false);
                items.AddItem(armor);
            }
            // cuirass
            if (Dice100.SuccessRoll(chance))
            {
                Items.DaggerfallUnityItem armor = Game.Items.ItemBuilder.CreateArmor(playerGender, race, Game.Items.Armor.Cuirass, Game.Items.ItemBuilder.RandomArmorMaterial(itemLevel));
                ItemEquipTable.EquipItem(armor, true, false);
                items.AddItem(armor);
            }
            // greaves
            if (Dice100.SuccessRoll(chance))
            {
                Items.DaggerfallUnityItem armor = Game.Items.ItemBuilder.CreateArmor(playerGender, race, Game.Items.Armor.Greaves, Game.Items.ItemBuilder.RandomArmorMaterial(itemLevel));
                ItemEquipTable.EquipItem(armor, true, false);
                items.AddItem(armor);
            }
            // boots
            if (Dice100.SuccessRoll(chance))
            {
                Items.DaggerfallUnityItem armor = Game.Items.ItemBuilder.CreateArmor(playerGender, race, Game.Items.Armor.Boots, Game.Items.ItemBuilder.RandomArmorMaterial(itemLevel));
                ItemEquipTable.EquipItem(armor, true, false);
                items.AddItem(armor);
            }

            // Initialize armor values to 100 (no armor)
            for (int i = 0; i < ArmorValues.Length; i++)
            {
                ArmorValues[i] = 100;
            }
            // Calculate armor values from equipment
            for (int i = (int)Game.Items.EquipSlots.Head; i < (int)Game.Items.EquipSlots.Feet; i++)
            {
                Items.DaggerfallUnityItem item = ItemEquipTable.GetItem((Items.EquipSlots)i);
                if (item != null && item.ItemGroup == Game.Items.ItemGroups.Armor)
                {
                    UpdateEquippedArmorValues(item, true);
                }
            }

            if (entityType == EntityTypes.EnemyClass)
            {
                // Clamp to maximum armor value of 60. In classic this also applies for monsters.
                // Note: Classic sets the value to 60 if it is > 50, which seems like an oversight.
                for (int i = 0; i < ArmorValues.Length; i++)
                {
                    if (ArmorValues[i] > 60)
                    {
                        ArmorValues[i] = 60;
                    }
                }
            }
            else
            {
                // Note: In classic, the above applies for equipment-using monsters as well as enemy classes.
                // The resulting armor values are often 60. Due to the +40 to hit against monsters this makes
                // monsters with equipment very easy to hit, and 60 is a worse value than any value monsters
                // have in their definition. To avoid this, in DF Unity the equipment values are only used if
                // they are better than the value in the definition.
                for (int i = 0; i < ArmorValues.Length; i++)
                {
                    if (ArmorValues[i] > (sbyte)(mobileEnemy.ArmorValue * 5))
                    {
                        ArmorValues[i] = (sbyte)(mobileEnemy.ArmorValue * 5);
                    }
                }
            }
When it comes to max armor values, AKA the current hit chance percentage:
The player maxes at 100 if they have 0 armor modifiers meaning 100% they will be hit by the enemy.
The Enemy maxes at 60 if they have 0 armor modifiers. meaning a 60% chance they will be hit by the player.

I imagine capping the enemy armor at 60 was to ensure the player has only a 60% hit chance no matter how weak the enemy is. I imagine this was for difficulty scaling and user playability/enjoyment. Make sure players don't get to board or its to easy to run through a dungeon.

Also, to understand what is happening with the lich, you need to understand there are two types of enemies in the engine.
1. Enemy Class; This is any humanoid based npc enemy. Say a redguard fighter or a darkelf mage.
2. Monster: This any non-humanoid npc, say a lich or a rat.

For the enemy class npcs, it merely sets them up like a player value wise.

However, monsters are a little different. Monsters have a base value armor that seems to vary between -10 and 10.

Monsters have an extra if then switch. This switch will activate anytime a monster has a valid armor value, It will take their monsters type base armor value and multiply it by 5. I imagine this was a coding shortcut to create boss type enemies and harder difficulty without having to create bunch of extra monster instances and so on.

So, if I am understanding the code right. The lich has a -10 armor value in EnemyBasics.cs. This negative value hits the if then switch. That -10 gets multiple by 5 giving the number -50. I still am unsure how it resolves the negative though. I can't find any coding explaining why it doesn't seem to transfer into the hitcode. I can't find any triggers to deal with negative armor values or anything that converts it into a positive. So, this is my best guess at this moment, and seems the most accurate as of now.

the only other explanation is it could of been a way to force monsters with equipment to default to their equipment armor value by ensuring it isn't a positive value. Original df had a bug where monsters with armor would default to worse armor value than what their base armor would be. Maybe this was a coding shortcut to ensure they used their armor or other modifiers?

Now, if the default armor for all enemies without equipment setups, like a lich, is 60, that means the final lich hit chance would be (60 - 50 = 10). This means you have only a 10% hit chance, at best, against a lich. Now, I believe this is before other hit modifiers, like skills and such would then boost the 10% up some. I won't lie, the negative value still is confusing me, and I can't figure out how it is resolved in the coding itself, since an sbyte variable technically stores negatives.

EnemyEntity.cs(Here is the specific code I am discussing above.)

Code: Select all

public void SetEnemyCareer(MobileEnemy mobileEnemy, EntityTypes entityType)
        {
            if (entityType == EntityTypes.EnemyMonster)
            {
                careerIndex = mobileEnemy.ID;
                career = GetMonsterCareerTemplate((MonsterCareers)careerIndex);
                stats.SetPermanentFromCareer(career);

                // Enemy monster has predefined level, health and armor values.
                // Armor values can be modified below by equipment.
                level = mobileEnemy.Level;
                maxHealth = UnityEngine.Random.Range(mobileEnemy.MinHealth, mobileEnemy.MaxHealth + 1);
                for (int i = 0; i < ArmorValues.Length; i++)
                {
                    ArmorValues[i] = (sbyte)(mobileEnemy.ArmorValue * 5);
                }
            }
            else if (entityType == EntityTypes.EnemyClass)
            {
                careerIndex = mobileEnemy.ID - 128;
                career = GetClassCareerTemplate((ClassCareers)careerIndex);
                stats.SetPermanentFromCareer(career);

                // Enemy class is levelled to player and uses similar health rules
                // City guards are 3 to 6 levels above the player
                level = GameManager.Instance.PlayerEntity.Level;
                if (careerIndex == (int)MobileTypes.Knight_CityWatch)
                    level += UnityEngine.Random.Range(3, 7);

                maxHealth = FormulaHelper.RollEnemyClassMaxHealth(level, career.HitPointsPerLevel);
            }
Another interesting find is all attacks are merely a 100 sided dice roll/ 100 integer random number generator. For a successful hit, the random number needs to be smaller than the end chancetohit number.

So, armor value increases your chance to hit value. As a result, everytime that 100 sided dice is rolled, it is less likely it will roll a number larger than your now modified armor value and tell the engine it was a successful hit. This works the same for enemies too. The higher their armor value, the less likely you'll roll above 100 to hit them.

FormulaHelper.cs (Here is the chance to hit initiation code)

Code: Select all

        /// </summary>
        public static int CalculateSuccessfulHit(DaggerfallEntity attacker, DaggerfallEntity target, int chanceToHitMod, int struckBodyPart)
        {
            if (attacker == null || target == null)
                return 0;

            Formula_2de_2i del;
            if (formula_2de_2i.TryGetValue("CalculateSuccessfulHit", out del))
                return del(attacker, target, chanceToHitMod, struckBodyPart);

            int chanceToHit = chanceToHitMod;
            PlayerEntity player = GameManager.Instance.PlayerEntity;
            EnemyEntity AITarget = target as EnemyEntity;

            int armorValue = 0;

            // Apply hit mod from character biography
            if (target == player)
            {
                chanceToHit -= player.BiographyAvoidHitMod;
            }

            // Get armor value for struck body part
            if (struckBodyPart <= target.ArmorValues.Length)
            {
                armorValue = target.ArmorValues[struckBodyPart] + target.IncreasedArmorValueModifier + target.DecreasedArmorValueModifier;
            }

            chanceToHit += armorValue;

            // Apply adrenaline rush modifiers.
            const int adrenalineRushModifier = 5;
            const int improvedAdrenalineRushModifier = 8;
            if (attacker.Career.AdrenalineRush && attacker.CurrentHealth < (attacker.MaxHealth / 8))
            {
                chanceToHit += (attacker.ImprovedAdrenalineRush) ? improvedAdrenalineRushModifier : adrenalineRushModifier;
            }

            if (target.Career.AdrenalineRush && target.CurrentHealth < (target.MaxHealth / 8))
            {
                chanceToHit -= (target.ImprovedAdrenalineRush) ? improvedAdrenalineRushModifier : adrenalineRushModifier;
            }

            // Apply enchantment modifier
            chanceToHit += attacker.ChanceToHitModifier;

            // Apply luck modifier.
            chanceToHit += ((attacker.Stats.LiveLuck - target.Stats.LiveLuck) / 10);

            // Apply agility modifier.
            chanceToHit += ((attacker.Stats.LiveAgility - target.Stats.LiveAgility) / 10);

            // Apply dodging modifier.
            // This modifier is bugged in classic and the attacker's dodging skill is used rather than the target's.
            // DF Chronicles says the dodging calculation is (dodging / 10), but it actually seems to be (dodging / 4).
            chanceToHit -= (target.Skills.GetLiveSkillValue(DFCareer.Skills.Dodging) / 4);

            // Apply critical strike modifier.
            if (Dice100.SuccessRoll(attacker.Skills.GetLiveSkillValue(DFCareer.Skills.CriticalStrike)))
            {
                chanceToHit += (attacker.Skills.GetLiveSkillValue(DFCareer.Skills.CriticalStrike) / 10);
            }

            // Apply monster modifier.
            if ((target != player) && (AITarget.EntityType == EntityTypes.EnemyMonster))
            {
                chanceToHit += 40;
            }

            // DF Chronicles says -60 is applied at the end, but it actually seems to be -50.
            chanceToHit -= 50;

            Mathf.Clamp(chanceToHit, 3, 97);

            if (Dice100.SuccessRoll(chanceToHit))
                return 1;
            else
                return 0;
        }
Dice100.cs (Heres the official roll code for if any skill or attack check will succeed or not, after all modifiers have been applied.)

Code: Select all

    public class Dice100
    {
        private Dice100()
        {
        }

        public static int Roll()
        {
            return Random.Range(1, 101);
        }

        public static bool SuccessRoll(int chanceSuccess)
        {
            return Random.Range(0, 100) < chanceSuccess; // Same as Random.Range(1, 101) <= chanceSuccess
        }

        public static bool FailedRoll(int chanceSuccess)
        {
            return Random.Range(0, 100) >= chanceSuccess; // Same as Random.Range(1, 101) > chanceSuccess
        }
    }
}
My Skyrim Mods: l3lessed Nexus Page

Daggerfall Unity mods: Combat Overhaul Mod

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

Post Reply