Research:Combat: Difference between revisions

From OpenMW Wiki
Jump to navigation Jump to search
 
(26 intermediate revisions by 3 users not shown)
Line 1: Line 1:
==Combat Formulae==
{{Template:Research Navbox}}


===Melee===


==Physical combat==


====Hit Chance====
===Attack swing===
{{Formula
{{Formula
|On melee contact
|On melee swing and on projectile launch
|Check applies to melee hits. Does not apply to projectiles or magic.
|
|{{StatusCol|red|not started yet}}
|{{StatusCol|green|Implemented}}
|{{StatusCol|red|Inaccurate: Fatigue is not accounted for at all; any kind of knockdown or other bonus is unaccounted for; blind, chameleon or other penalties are not accounted for}}}}
|{{StatusCol|green|Verified}}}}
 
Swing is the amount of time the weapon is held back for attack. The damage range of a weapon is directly related to how long the attack key is held. An instant click will result in minimum stated damage, while waiting until the attack wind-up animation is fully complete will result in maximum stated damage.
 
For the player:
<syntaxhighlight lang="python">
attackSwing = attack key time held / full attack wind-up animation length
</syntaxhighlight>
 
For NPCs and creatures:
<syntaxhighlight lang="python">
attackSwing = min(1, 0.1 + 0.01 * (roll 100))
</syntaxhighlight>
 
 
===Damage===
{{Formula
|On melee swing and on projectile launch
|Potential damage output from an attack.
|{{StatusCol|green|Implemented}}
|{{StatusCol|green|Verified}}}}
<syntaxhighlight lang="python">
attackType is one of {chop, slash, thrust}, or equivalent for creatures
 
if attack is from a creature (non-biped):
    damageMin = creature.attack[attackType].damageMin
    damageMax = creature.attack[attackType].damageMax
elif attack is from a melee or ranged weapon:
    damageMin = weapon.attack[attackType].damageMin
    damageMax = weapon.attack[attackType].damageMax
 
    # note for thrown weapons: weapon and ammo are the same object which match, causing 2x base damage
    if weapon.isMarksman and ammo that matches the weapon type is available:
        damageMin += ammo.damageMin
        damageMax += ammo.damageMax
elif attack is hand-to-hand:
    if actor.isWerewolf:
        claw = script global float "WerewolfClawMult"
        damageMin = int(fMinHandToHandMult * actor.handToHandSkill * claw)
        damageMax = int(fMaxHandToHandMult * actor.handToHandSkill * claw)
    else:
        damageMin = int(fMinHandToHandMult * actor.handToHandSkill)
        damageMax = int(fMaxHandToHandMult * actor.handToHandSkill)
 
damageMax = max(damageMin, damageMax)
rawDamage = damageMin + actor.attackSwing * (damageMax - damageMin)
 
if attack is from a melee or ranged weapon:  # see comments
    rawDamage *= 0.5 + 0.01 * actor.strength
    rawDamage *= weapon.condition / weapon.maxCondition
</syntaxhighlight>
 
====Comments====
Weapon strength scaling does not use existing scaling GMSTs, and hand-to-hand does not exhibit any strength scaling. MCP uses fDamageStrengthBase + 0.1 * fDamageStrengthMult * actor.strength for weapon scaling.
 
===Hit Chance===
{{Formula
|On melee and ranged contact
|Check applies to melee and projectile hits. isAware is the awareness check from [[Research:NPC_AI_Behaviour#NPC_Awareness_Check|NPC AI Behaviour]].
|{{StatusCol|green|Implemented}}
|{{StatusCol|green|Verified}}}}
<syntaxhighlight lang="python">
hitTest :: (isProjectile, attacker, defender) -> hit
 
fatigueTerm = fFatigueBase - fFatigueMult*(1 - normalisedFatigue)
where normalisedFatigue is a function of fatigue. empty fatigue bar -> 0.0, full fatigue bar -> 1.0
 
# the attacker may switch weapons while a projectile is in flight
skill = isProjectile ? attacker.marksmanSkill : attacker.skill[attacker.weapon.relatedSkill]
attackTerm = (skill + 0.2 * attacker.agility + 0.1 * attacker.luck) * atttacker.fatigueTerm
attackTerm += attacker.attackBonus - attacker.blind
 
defenseTerm = 0
 
if defender.fatigue >= 0:
    unaware = (not defender.inCombat) and attacker is player and (not defender.isAware(player)) # see comments
    if not (defender.isKnockedDown or defender.isParalyzed or unaware):
        defenseTerm = (0.2 * defender.agility + 0.1 * defender.luck) * defender.fatigueTerm
        defenseTerm += min(100, defender.sanctuary)
 
    defenseTerm += min(100, fCombatInvisoMult * defender.chameleon)
    defenseTerm += min(100, fCombatInvisoMult * defender.invisibility)
 
x = round(attackTerm - defenseTerm)
 
if x > 0 and roll 100 < x: return hit
else: return miss
</syntaxhighlight>
 
====Comments====
 
The unaware check is only calculated correctly for projectile attacks. Due to either a design decision or bug, it doesn't apply to melee attacks, but it seems likely to be an design bug. It is recommended to implement it for all cases.
 
===Hit reactions===
{{Formula
|After a successful hit, before standard damage or blocking is applied
|Reactionary effects of a hit; effects are applied in order.
|{{StatusCol|green|Implemented}}
|{{StatusCol|orange|Includes confirmed mechanics, but there may be further undocumented effects.}}}}
 
====Weapon enchantment====
<syntaxhighlight lang="python">
if exists weapon.enchantment and weapon.enchantment is cast on strike:
    apply weapon.enchantment
</syntaxhighlight>
 
====Elemental shield====
Elemental shields are similar to a thorns effect, causing damage to the attacker.
<syntaxhighlight lang="python">
saveTerm = attacker.destruction + 0.2 * attacker.willpower + 0.1 * attacker.luck
saveTerm *= 1.25 * attacker.normalisedFatigue
 
for each elementalShield in defender.activeEffects:
    x = max(0, saveTerm - roll float 100)
    x = min(100, x + attacker.elementalResist[elementalShield.element])
    x = fElementalShieldMult * elementalShield.magnitude * (1 - 0.01 * x)
    attacker takes x damage
</syntaxhighlight>
 
Elemental shields do not stack, each effect instance is rolled separately.
 
====Disease transfer====
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
hit chance = Attackers weapon skill * 1.25 + Attackers Attack Bonus - Defenders Sanctuary Bonus
if defender is not player: return
  + (Attackers Agility - Defenders Agility) * 0.25 + (Attackers Luck - Defenders Luck) * 0.125
 
for each disease in attacker.activeSpells:
    if any of the disease.effects is corprus:
        resist = 1 - 0.01 * defender.resistCorprus
    elif spell.castType == disease:
        resist = 1 - 0.01 * defender.resistDisease
    elif spell.castType == blight:
        resist = 1 - 0.01 * defender.resistBlight
    else:
        continue
 
    if player already has the disease:
        continue
       
    x = int(fDiseaseXferChance * 100 * resist)
    if roll 10000 < x:
        defender acquires disease
        display message sMagicContractDisease with disease name
</syntaxhighlight>
</syntaxhighlight>




====Blocking with a shield====
===Blocking with a shield===
{{Formula
{{Formula
|On melee contact
|On melee contact
|With a shield equipped, blocking has a chance to negate all damage.
|With a shield equipped, blocking has a chance to negate all damage. Uses attacker's [[Research:Combat#Attack_swing|attackSwing]].
|{{StatusCol|red|not started yet}}
|{{StatusCol|green|Implemented}}
|{{StatusCol|green|Verified}}}}
|{{StatusCol|green|Verified}}}}
''On enemy hit''
''On enemy hit''
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
if player is knocked down, knocked out, paralyzed, hitstunned, in cast stance or is casting: no block
theta = angle from player to enemy, negative is enemy left of centreline, positive is enemy right of centreline
theta = angle from player to enemy, negative is enemy left of centreline, positive is enemy right of centreline


Line 34: Line 174:


blockTerm = pcBlockSkill + 0.2 * pcAgility + 0.1 * pcLuck
blockTerm = pcBlockSkill + 0.2 * pcAgility + 0.1 * pcLuck
enemySwing = random number in range [0.1, 1]
swingTerm = attackSwing * fSwingBlockMult + fSwingBlockBase
swingTerm = enemySwing * fSwingBlockMult + fSwingBlockBase


fatigueTerm = fFatigueBase - fFatigueMult*(1 - normalisedFatigue)
fatigueTerm = fFatigueBase - fFatigueMult*(1 - normalisedFatigue)
Line 61: Line 200:
</syntaxhighlight>
</syntaxhighlight>


====Comments====
The enemySwing variable is effectively how much the enemy has charged their attack. It seems to be uniform random and is not based on weapon damage. The other thing of note is the playerTerm bonus. The fBlockStillBonus GMST is not used in vanilla Morrowind (the multiplier is hard-coded), but is used in OpenMW. Unexpectedly, this bonus is still given if the player is moving backwards or strafing, only moving forward will negate it.


The enemySwing variable is effectively how much the enemy has charged their attack. It seems to be uniform random and is not based on weapon damage. The other thing of note is the playerTerm bonus. The fBlockStillBonus GMST is not used at all; the multiplier is hard-coded. Unexpectedly, this bonus is still given if the player is moving backwards or strafing, only moving forward will negate it.
===Bodypart hit chance===
 
 
====Bodypart Hit Chance====
{{Formula
{{Formula
|On melee hit
|On melee or ranged hit
|Damage is directed towards a single random body part. Occurs once a hit is confirmed.
|Damage is directed towards a single random body part. Occurs once a hit is confirmed.
|{{StatusCol|green|Implemented}}
|{{StatusCol|green|Implemented}}
Line 91: Line 229:
|}
|}


Note that you still take unarmored class hits to the shield arm, if you don't have a shield equipped.
Note that you still take unarmored class hits to the shield arm if you don't have a shield equipped. This includes when the actor is holding a 2-handed weapon.


====Damage====
 
===Physical contact===
{{Formula
{{Formula
|On melee hit
|On melee or ranged hit, before mitigation
|Occurs pre armor mitigation.
|Damage is directed towards a single random body part. Occurs once a hit is confirmed.
|{{StatusCol|green|implemented}}
|{{StatusCol|green|Implemented}}
|{{StatusCol|red|Sources of melee damage and critical hits are unexplained}}}}
|{{StatusCol|green|Verified}}}}
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
if actor is knocked down: damage *= fCombatKODamageMult
damage = rawDamage
if actor is critically hit: damage *= fCombatCriticalStrikeMult
unaware = (not defender.inCombat) and attacker is player and (not defender.isAware(player))
 
if defender is knocked down or knocked out:
    damage *= fCombatKODamageMult
if unaware and attack is not projectile damage:
    damage *= fCombatCriticalStrikeMult
    display message sTargetCriticalStrike
if unaware:
    use critical strike hit audio
 
if attack is hand-to-hand:
    if attacker.isWerewolf or defender is knocked down, knocked out or paralyzed:
        damage *= fHandtoHandHealthPer
        damage is applied to health as normal
    else:
        damage is applied to fatigue, ignoring all mitigation and difficulty scaling
 
if attack uses a weapon:
    if weapon is (not enchanted and does not ignore normal resistance):
        damage *= (1 - 0.01 * actor.resistNormalWeapons)
        if actor.resistNormalWeapons == 100:
            display sMagicTargetResistsWeapons
 
    if actor is a werewolf:
        if (projectile and projectile.isSilver) or (melee and weapon.isSilver):
            damage *= fWerewolfSilverWeaponDamageMult
</syntaxhighlight>
</syntaxhighlight>


====Armor Mitigation====
====Comments====
Note that projectile attacks cannot crit, but they do make the critical hit sound, as the sound code is separated.
 
===Armor mitigation===
{{Formula
{{Formula
|On melee hit
|On melee or ranged hit
|Occurs once a hit is confirmed.
|Occurs once a hit is confirmed.
|{{StatusCol|red|not started yet}}
|{{StatusCol|green|Implemented}}
|{{StatusCol|red|Inaccurate: AR is underspecified (vs armor part/unarmored/creature); damage to armor is unspecified; shield spell effects are underspecified}}}}
|{{StatusCol|green|Verified}}}}
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
if damage < 0.001: skip mitigation to avoid divide by zero, set damage to 0
Physical damage applied to a target actor
AR = target overall armor rating including shield spells
 
x = damage / (damage + target armor rating)
if damage < 0.001: skip rest of mitigation, set damage to 0
 
unmitigatedDamage = damage
 
x = damage / (damage + actor.effectiveArmorRating)
damage *= max(fCombatArmorMinMult, x)
damage *= max(fCombatArmorMinMult, x)
z = int(unmitigatedDamage - damage)
if damage < 1: damage = 1
armour = weighted selection from actor armour slots
if armour is equipped there, armour loses z condition
if actor is player: player exercises relevant armour skill for armour
</syntaxhighlight>
</syntaxhighlight>


====Comments====
It appears that the volume of the armour hit sound is proportional to the enemy attackSwing.


====Knockdowns====
===Knockdowns===
{{Formula
{{Formula
|On melee hit
|On melee or ranged hit
|Occurs after damage is taken.
|Occurs after damage is taken.
|{{StatusCol|green|implemented}}
|{{StatusCol|green|implemented}}
|{{StatusCol|green|Verified}}}}
|{{StatusCol|green|Verified}}}}
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
damage = incoming damage before armour reduction
damage = incoming damage before armour reduction (unmitigatedDamage)
agilityTerm = agility * fKnockDownMult
agilityTerm = agility * fKnockDownMult
knockdownTerm = agility * iKnockDownOddsMult * 0.01 + iKnockDownOddsBase
knockdownTerm = agility * iKnockDownOddsMult * 0.01 + iKnockDownOddsBase
Line 132: Line 311:
</syntaxhighlight>
</syntaxhighlight>


===Ranged===


===Hitstun===
{{Formula
|On physical contact
|Occurs after damage is taken.
|{{StatusCol|red|not started}}
|{{StatusCol|orange|Verified, but there may be further interactions}}}}
<syntaxhighlight lang="python">
if actor is creature and its animation is attacking, casting, or lockpicking/probing: no stun
# players or NPCs always take hitstun


====Hit chance====
apply hitstun if not already in hitstun
</syntaxhighlight>


====Damage====
====Comments====
Hitstun is the actor state, with accompanying animations (Hit1-5 or SwimHit1-3) and movement restriction that occurs when the actor is hit. Hitstun prevents movement during its animation, while still allowing turning, crouching and jumping. It also prevents initiating new attacks or casting, but allows attacks (including releasing the attack button to strike) or casting to complete.


====Projectile recovery====
===Weapon wear===
{{Formula
|On weapon contact, both on hits and on missed hit roll
|Weapon condition loss from combat
|{{StatusCol|green|Implemented}}
|{{StatusCol|green|Verified}}}}
<syntaxhighlight lang="python">
if attack missed: rawDamage = 0
x = max(1, fWeaponDamageMult * rawDamage)
weapon loses x condition
</syntaxhighlight>


====Comments====
This occurs on the same conditions that cause the weapon "miss" sound, as well as hits. Uses rawDamage, not mitigated damage. Should be evaluated after the attack is complete, as it may break the weapon.


==Armor Rating and Armor Class==
==Armor rating and Armor class==


===Determining armor class===
===Determining armor class===
Line 149: Line 349:
|On evaluating AR
|On evaluating AR
|
|
|{{StatusCol|green|implemented, epsilon not needed?}}
|{{StatusCol|green|Implemented}}
|{{StatusCol|green|Verified}}}}
|{{StatusCol|green|Verified}}}}
Item armor class:
Item armor class:
Line 166: Line 366:
|When evaluating effective armor rating
|When evaluating effective armor rating
|
|
|{{StatusCol|green|Implemented}}
|{{StatusCol|green|Implemented, bug fixed}}
|{{StatusCol|green|Verified}}}}
|{{StatusCol|green|Verified}}}}
For NPCs or the player:
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
armorSkill is either npc lightArmorSkill, mediumArmorSkill or heavyArmorSkill dependent on item class
armorSkill is either npc lightArmorSkill, mediumArmorSkill or heavyArmorSkill dependent on item class
if the actor has no armor at all: effectiveRating = 0    # obvious bug


if there is armor in a slot:
if there is armor in a slot:
if itemWeight == 0: rating = armorBaseRating
    if itemWeight == 0: rating = armorBaseRating
else: rating = armorBaseRating * armorSkill / iBaseArmorSkill
    else: rating = armorBaseRating * armorSkill / iBaseArmorSkill


otherwise unarmored skill is used instead:
otherwise unarmored skill is used instead:
rating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill)
    rating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill)


totalRating is a weighted combination of slot ratings:
effectiveRating is a weighted combination of slot ratings:
totalRating = Cuirass * 0.3 + (Shield + Helm + Greaves + Boots + LPauldron + RPauldron) * 0.1
effectiveRating = Cuirass * 0.3 + (Shield + Helm + Greaves + Boots + LPauldron + RPauldron) * 0.1
   + (LGauntlet + RGauntlet) * 0.05 + Shield spell effect magnitude
   + (LGauntlet + RGauntlet) * 0.05 + Shield spell effect magnitude
</syntaxhighlight>
</syntaxhighlight>


For creatures:
<syntaxhighlight lang="python">
effectiveRating = 0    # also a bug
should be:


Note that elemental shield spells do not contribute towards armor.
effectiveRating = Shield spell effect magnitude
</syntaxhighlight>


==Item durability==
====Comments====
Without fixes, if an actor has no armour equipped at all the game would return an armour rating of 0, ignoring any unarmored skill. Similarly, creatures would always have zero rating, even ones that have active shield spells. Note that elemental shield spells do not contribute towards armor.


===Damage Taken===
==Difficulty multiplier==
{{Formula
{{Formula
|On melee hit, weapons and armor
|On application of health damage after all other mechanics.
|Occurs once a hit is confirmed.
|Difficulty scaling of physical combat damage (including elemental shield damage). Fatigue damage or spell effects do not scale.
|{{StatusCol|green|Implemented}}
|{{StatusCol|green|Implemented}}
|{{StatusCol|orange|Mostly accurate but underspecified}}}}
|{{StatusCol|green|Verified}}}}
With every successful hit a weapon takes fWeaponDamageMult: (default 0.1) percentage of damage rounded down. Minimum damage to weapon = 1 With weapon health going down, its damage is also going down, thus in turn slowing the rate of damaging weapon. Armor takes an amount of damage that it stopped. Only armor part that took a hit takes any damage.
<syntaxhighlight lang="python">
difficulty : int [-100..100]
difficultyTerm = 0.01 * difficulty
 
if defender is player:
    if difficultyTerm > 0:
        x = fDifficultyMult * difficultyTerm
    else:
        x = difficultyTerm / fDifficultyMult
elif attacker is player:
    if difficultyTerm > 0:
        x = -difficultyTerm / fDifficultyMult
    else:
        x = fDifficultyMult * -difficultyTerm
else:
    x = 0
 
damage *= 1 + x
</syntaxhighlight>
 
 
 
 
{{Template:Research Navbox}}

Latest revision as of 20:27, 8 September 2017


Physical combat

Attack swing

Actions affected On melee swing and on projectile launch
Description
Implementation status Implemented
Analysis status Verified

Swing is the amount of time the weapon is held back for attack. The damage range of a weapon is directly related to how long the attack key is held. An instant click will result in minimum stated damage, while waiting until the attack wind-up animation is fully complete will result in maximum stated damage.

For the player:

attackSwing = attack key time held / full attack wind-up animation length

For NPCs and creatures:

attackSwing = min(1, 0.1 + 0.01 * (roll 100))


Damage

Actions affected On melee swing and on projectile launch
Description Potential damage output from an attack.
Implementation status Implemented
Analysis status Verified
attackType is one of {chop, slash, thrust}, or equivalent for creatures

if attack is from a creature (non-biped):
    damageMin = creature.attack[attackType].damageMin
    damageMax = creature.attack[attackType].damageMax
elif attack is from a melee or ranged weapon:
    damageMin = weapon.attack[attackType].damageMin
    damageMax = weapon.attack[attackType].damageMax

    # note for thrown weapons: weapon and ammo are the same object which match, causing 2x base damage
    if weapon.isMarksman and ammo that matches the weapon type is available:
        damageMin += ammo.damageMin
        damageMax += ammo.damageMax
elif attack is hand-to-hand:
    if actor.isWerewolf:
        claw = script global float "WerewolfClawMult"
        damageMin = int(fMinHandToHandMult * actor.handToHandSkill * claw)
        damageMax = int(fMaxHandToHandMult * actor.handToHandSkill * claw)
    else:
        damageMin = int(fMinHandToHandMult * actor.handToHandSkill)
        damageMax = int(fMaxHandToHandMult * actor.handToHandSkill)

damageMax = max(damageMin, damageMax)
rawDamage = damageMin + actor.attackSwing * (damageMax - damageMin)

if attack is from a melee or ranged weapon:   # see comments
    rawDamage *= 0.5 + 0.01 * actor.strength
    rawDamage *= weapon.condition / weapon.maxCondition

Comments

Weapon strength scaling does not use existing scaling GMSTs, and hand-to-hand does not exhibit any strength scaling. MCP uses fDamageStrengthBase + 0.1 * fDamageStrengthMult * actor.strength for weapon scaling.

Hit Chance

Actions affected On melee and ranged contact
Description Check applies to melee and projectile hits. isAware is the awareness check from NPC AI Behaviour.
Implementation status Implemented
Analysis status Verified
hitTest :: (isProjectile, attacker, defender) -> hit

fatigueTerm = fFatigueBase - fFatigueMult*(1 - normalisedFatigue)
where normalisedFatigue is a function of fatigue. empty fatigue bar -> 0.0, full fatigue bar -> 1.0

# the attacker may switch weapons while a projectile is in flight
skill = isProjectile ? attacker.marksmanSkill : attacker.skill[attacker.weapon.relatedSkill]
attackTerm = (skill + 0.2 * attacker.agility + 0.1 * attacker.luck) * atttacker.fatigueTerm
attackTerm += attacker.attackBonus - attacker.blind

defenseTerm = 0

if defender.fatigue >= 0:
    unaware = (not defender.inCombat) and attacker is player and (not defender.isAware(player)) # see comments
    if not (defender.isKnockedDown or defender.isParalyzed or unaware):
        defenseTerm = (0.2 * defender.agility + 0.1 * defender.luck) * defender.fatigueTerm
        defenseTerm += min(100, defender.sanctuary)

    defenseTerm += min(100, fCombatInvisoMult * defender.chameleon)
    defenseTerm += min(100, fCombatInvisoMult * defender.invisibility)

x = round(attackTerm - defenseTerm)

if x > 0 and roll 100 < x: return hit
else: return miss

Comments

The unaware check is only calculated correctly for projectile attacks. Due to either a design decision or bug, it doesn't apply to melee attacks, but it seems likely to be an design bug. It is recommended to implement it for all cases.

Hit reactions

Actions affected After a successful hit, before standard damage or blocking is applied
Description Reactionary effects of a hit; effects are applied in order.
Implementation status Implemented
Analysis status Includes confirmed mechanics, but there may be further undocumented effects.

Weapon enchantment

if exists weapon.enchantment and weapon.enchantment is cast on strike:
    apply weapon.enchantment

Elemental shield

Elemental shields are similar to a thorns effect, causing damage to the attacker.

saveTerm = attacker.destruction + 0.2 * attacker.willpower + 0.1 * attacker.luck
saveTerm *= 1.25 * attacker.normalisedFatigue

for each elementalShield in defender.activeEffects:
    x = max(0, saveTerm - roll float 100)
    x = min(100, x + attacker.elementalResist[elementalShield.element])
    x = fElementalShieldMult * elementalShield.magnitude * (1 - 0.01 * x)
    attacker takes x damage

Elemental shields do not stack, each effect instance is rolled separately.

Disease transfer

if defender is not player: return

for each disease in attacker.activeSpells:
    if any of the disease.effects is corprus:
        resist = 1 - 0.01 * defender.resistCorprus
    elif spell.castType == disease:
        resist = 1 - 0.01 * defender.resistDisease
    elif spell.castType == blight:
        resist = 1 - 0.01 * defender.resistBlight
    else:
        continue

    if player already has the disease:
        continue
        
    x = int(fDiseaseXferChance * 100 * resist)
    if roll 10000 < x:
        defender acquires disease
        display message sMagicContractDisease with disease name


Blocking with a shield

Actions affected On melee contact
Description With a shield equipped, blocking has a chance to negate all damage. Uses attacker's attackSwing.
Implementation status Implemented
Analysis status Verified

On enemy hit

if player is knocked down, knocked out, paralyzed, hitstunned, in cast stance or is casting: no block

theta = angle from player to enemy, negative is enemy left of centreline, positive is enemy right of centreline

if theta < fCombatBlockLeftAngle: no block
if theta > fCombatBlockRightAngle: no block

#Note that the above tests are inaccurate, as each comparison is calculated using a dot product
#which is converted to an angle improperly. The effective value of fCombatBlock*Angle ends up
#slightly different than specified.

blockTerm = pcBlockSkill + 0.2 * pcAgility + 0.1 * pcLuck
swingTerm = attackSwing * fSwingBlockMult + fSwingBlockBase

fatigueTerm = fFatigueBase - fFatigueMult*(1 - normalisedFatigue)
where normalisedFatigue is a function of fatigue. empty fatigue bar -> 0.0, full fatigue bar -> 1.0
Note fatigueTerm is normally 1.25 at full fatigue.

playerTerm = blockTerm * swingTerm
if player is not moving forward at any speed: playerTerm = playerTerm * 1.25 (note this is not fBlockStillBonus)
playerTerm = playerTerm * fatigueTerm

if npc is a creature: npcSkill = creature combat stat
otherwise: npcSkill = npc skill with wielded weapon

npcTerm = npcSkill + 0.2 * npcAgility + 0.1 * npcLuck
npcFatigueTerm = fFatigueBase - fFatigueMult*(1 - normalisedFatigue)
using NPC normalisedFatigue
npcTerm = npcTerm * npcFatigueTerm

x = int(playerTerm - npcTerm)
if x < iBlockMinChance: x = iBlockMinChance
if x > iBlockMaxChance: x = iBlockMaxChance
roll 100, block if roll < x

if a hit is blocked, the shield durability is reduced by incoming damage, no damage mitigation is applied

Comments

The enemySwing variable is effectively how much the enemy has charged their attack. It seems to be uniform random and is not based on weapon damage. The other thing of note is the playerTerm bonus. The fBlockStillBonus GMST is not used in vanilla Morrowind (the multiplier is hard-coded), but is used in OpenMW. Unexpectedly, this bonus is still given if the player is moving backwards or strafing, only moving forward will negate it.

Bodypart hit chance

Actions affected On melee or ranged hit
Description Damage is directed towards a single random body part. Occurs once a hit is confirmed.
Implementation status Implemented
Analysis status Verified
Chest/Cuirass 30%
Head/Helm 10%
Legs/Greaves 10%
Feet/Boots 10%
L Shoulder/L Pauldron 10%
R Shoulder/R Pauldron 10%
Shield arm/Shield 10%
L Hand/L Gauntlet 5%
R Hand/R Gauntlet 5%

Note that you still take unarmored class hits to the shield arm if you don't have a shield equipped. This includes when the actor is holding a 2-handed weapon.


Physical contact

Actions affected On melee or ranged hit, before mitigation
Description Damage is directed towards a single random body part. Occurs once a hit is confirmed.
Implementation status Implemented
Analysis status Verified
damage = rawDamage
unaware = (not defender.inCombat) and attacker is player and (not defender.isAware(player))

if defender is knocked down or knocked out:
    damage *= fCombatKODamageMult
if unaware and attack is not projectile damage:
    damage *= fCombatCriticalStrikeMult
    display message sTargetCriticalStrike
if unaware:
    use critical strike hit audio

if attack is hand-to-hand:
    if attacker.isWerewolf or defender is knocked down, knocked out or paralyzed:
        damage *= fHandtoHandHealthPer
        damage is applied to health as normal
    else:
        damage is applied to fatigue, ignoring all mitigation and difficulty scaling

if attack uses a weapon:
    if weapon is (not enchanted and does not ignore normal resistance):
        damage *= (1 - 0.01 * actor.resistNormalWeapons)
        if actor.resistNormalWeapons == 100:
            display sMagicTargetResistsWeapons

    if actor is a werewolf:
        if (projectile and projectile.isSilver) or (melee and weapon.isSilver):
            damage *= fWerewolfSilverWeaponDamageMult

Comments

Note that projectile attacks cannot crit, but they do make the critical hit sound, as the sound code is separated.

Armor mitigation

Actions affected On melee or ranged hit
Description Occurs once a hit is confirmed.
Implementation status Implemented
Analysis status Verified
Physical damage applied to a target actor

if damage < 0.001: skip rest of mitigation, set damage to 0

unmitigatedDamage = damage

x = damage / (damage + actor.effectiveArmorRating)
damage *= max(fCombatArmorMinMult, x)
z = int(unmitigatedDamage - damage)
if damage < 1: damage = 1

armour = weighted selection from actor armour slots
if armour is equipped there, armour loses z condition
if actor is player: player exercises relevant armour skill for armour

Comments

It appears that the volume of the armour hit sound is proportional to the enemy attackSwing.

Knockdowns

Actions affected On melee or ranged hit
Description Occurs after damage is taken.
Implementation status implemented
Analysis status Verified
damage = incoming damage before armour reduction (unmitigatedDamage)
agilityTerm = agility * fKnockDownMult
knockdownTerm = agility * iKnockDownOddsMult * 0.01 + iKnockDownOddsBase

roll 100, knockdown occurs if agilityTerm <= damage and knockdownTerm <= roll


Hitstun

Actions affected On physical contact
Description Occurs after damage is taken.
Implementation status not started
Analysis status Verified, but there may be further interactions
if actor is creature and its animation is attacking, casting, or lockpicking/probing: no stun
# players or NPCs always take hitstun

apply hitstun if not already in hitstun

Comments

Hitstun is the actor state, with accompanying animations (Hit1-5 or SwimHit1-3) and movement restriction that occurs when the actor is hit. Hitstun prevents movement during its animation, while still allowing turning, crouching and jumping. It also prevents initiating new attacks or casting, but allows attacks (including releasing the attack button to strike) or casting to complete.

Weapon wear

Actions affected On weapon contact, both on hits and on missed hit roll
Description Weapon condition loss from combat
Implementation status Implemented
Analysis status Verified
if attack missed: rawDamage = 0
x = max(1, fWeaponDamageMult * rawDamage)
weapon loses x condition

Comments

This occurs on the same conditions that cause the weapon "miss" sound, as well as hits. Uses rawDamage, not mitigated damage. Should be evaluated after the attack is complete, as it may break the weapon.

Armor rating and Armor class

Determining armor class

Actions affected On evaluating AR
Description
Implementation status Implemented
Analysis status Verified

Item armor class:

epsilon = 5e-4
referenceWeight = iHelmWeight, iPauldronWeight, iCuirassWeight, iGauntletWeight, iGreavesWeight, iBootsWeight or iShieldWeight depending on slot

if itemWeight == 0: return NONE
if itemWeight <= referenceWeight * fLightMaxMod + epsilon: return LIGHT
if itemWeight <= referenceWeight * fMedMaxMod + epsilon: return MEDIUM
else: return HEAVY

Armor rating

Actions affected When evaluating effective armor rating
Description
Implementation status Implemented, bug fixed
Analysis status Verified

For NPCs or the player:

armorSkill is either npc lightArmorSkill, mediumArmorSkill or heavyArmorSkill dependent on item class

if the actor has no armor at all: effectiveRating = 0    # obvious bug

if there is armor in a slot:
    if itemWeight == 0: rating = armorBaseRating
    else: rating = armorBaseRating * armorSkill / iBaseArmorSkill

otherwise unarmored skill is used instead:
    rating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill)

effectiveRating is a weighted combination of slot ratings:
effectiveRating = Cuirass * 0.3 + (Shield + Helm + Greaves + Boots + LPauldron + RPauldron) * 0.1
  + (LGauntlet + RGauntlet) * 0.05 + Shield spell effect magnitude

For creatures:

effectiveRating = 0    # also a bug

should be:

effectiveRating = Shield spell effect magnitude

Comments

Without fixes, if an actor has no armour equipped at all the game would return an armour rating of 0, ignoring any unarmored skill. Similarly, creatures would always have zero rating, even ones that have active shield spells. Note that elemental shield spells do not contribute towards armor.

Difficulty multiplier

Actions affected On application of health damage after all other mechanics.
Description Difficulty scaling of physical combat damage (including elemental shield damage). Fatigue damage or spell effects do not scale.
Implementation status Implemented
Analysis status Verified
difficulty : int [-100..100]
difficultyTerm = 0.01 * difficulty

if defender is player:
    if difficultyTerm > 0:
        x = fDifficultyMult * difficultyTerm
    else:
        x = difficultyTerm / fDifficultyMult
elif attacker is player:
    if difficultyTerm > 0:
        x = -difficultyTerm / fDifficultyMult
    else:
        x = fDifficultyMult * -difficultyTerm
else:
    x = 0

damage *= 1 + x