Research:Stats and Levelling

From OpenMW Wiki
Jump to navigation Jump to search

{{#switch:|subgroup|child=|none=|#default=

}}


Actor dynamic stats[edit]

Actions affected Recalculated when base attributes are modified
Description Health points, magicka, fatigue, and encumbrance
Implementation status implemented
Analysis status Requires verification

HP[edit]

Initial HP <syntaxhighlight lang="python"> health = int(0.5 * (baseStrength + baseEndurance)) </syntaxhighlight>

At every level-up <syntaxhighlight lang="python"> occurs after attributes have been increased by the level-up bonusHP = fLevelUpHealthEndMult * baseEndurance </syntaxhighlight>


Magicka multiplier[edit]

The non-visible stat used to stack magicka modifiers. Affected by NPC type and the Fortify Magicka Multiplier effect.

On actor creation <syntaxhighlight lang="python"> if actor is the player:

   magickaMultiplier = fPCbaseMagickaMult

else:

   magickaMultiplier = fNPCbaseMagickaMult

</syntaxhighlight>

When a Fortify Magicka Multiplier effect is applied <syntaxhighlight lang="python"> magickaMultiplier is adjusted by 0.1 * effect.magnitude </syntaxhighlight>


Magicka points[edit]

Every time intelligence or magicka multiplier is modified <syntaxhighlight lang="python"> maxMagicka = magickaMultiplier * intelligence currentMagicka is rescaled to preserve % left </syntaxhighlight>


Fatigue[edit]

Recovery every frame <syntaxhighlight lang="python"> if currentFatigue < maxFatigue:

   x = fFatigueReturnBase + fFatigueReturnMult * endurance
   recover (frameTime * x) fatigue

</syntaxhighlight>

Every time a base stat is modified <syntaxhighlight lang="python"> maxFatigue = strength + willpower + agility + endurance currentFatigue is rescaled to preserve % left </syntaxhighlight>


Encumbrance[edit]

Every time strength is modified <syntaxhighlight lang="python"> maxEncumbrance = fEncumbranceStrMult * strength </syntaxhighlight>

Resting[edit]

Actions affected On resting/waiting
Description Waiting also applies to time passed after training. Resting also applies to time passed during travel. Uses common term normalizedEncumbrance.
Implementation status Implemented, interruptingCreatures bug fixed, interruptAtHoursRemaining bug left in for compatibility
Analysis status Verified, but sleep interruption has bugs

<syntaxhighlight lang="python"> if resting in an exterior cell and the region has a sleep creature leveled list:

   x = roll hoursRested
   y = fSleepRandMod * hoursRested
   if x < y:
       interruptAtHoursRemaining = int(fSleepRestMod * hoursRested)
       interruptingCreatures = max(1, roll iNumberCreatures)
       sleep will only last (hoursRested - interruptAtHoursRemaining) hours
       unless interruptAtHoursRemaining == 0, then no interruption occurs       # contains bug
       sleep will be interrupted with 1 creature from the region leveled list   # contains bug


for each hour:

   for every actor in the entire world:
       if resting, not waiting:
           health += 0.1 * endurance
           if actor does not have magic effect Stunted Magicka:
               magicka += fRestMagicMult * intelligence
       x = fFatigueReturnBase + fFatigueReturnMult * (1 - normalizedEncumbrance)
       x *= fEndFatigueMult * endurance
       fatigue += 3600 * x

</syntaxhighlight>


Comments[edit]

Resting allows all actors in the game to recover, not just the player. There are multiple problems with interrupted rest.

If the value of interruptAtHoursRemaining is 0, which occurs when hoursRested <= 3 with default GMSTs, then the game fails to execute the interruption encounter as the rest ends, in effect making short rests completely safe.

The code only spawns the first creature it finds in the leveled list, as well as calculating the number of creatures incorrectly. interruptingCreatures should be 1 + roll iNumberCreatures, and that number of creatures should be spawned.

Finally, the design of the interruption test has hoursRested on both sides of the test. This makes the probability of being interrupted independent of the number of hours slept, except for quantization effects.

Player levelling[edit]

Skill progress[edit]

Actions affected On exercising a skill
Description
Implementation status Implemented
Analysis status Verified

Skills have a progression metric that is increased by player actions. Each action has an associated skill gain factor present in the skill record from the ESM. The progression is usually, but not always increased by that gain factor each time an action is performed. Players in werewolf form do not gain progress at all.


On successful barter <syntaxhighlight lang="python"> skillGain = skill.gainFactor[0] # "Successful Bargain"

if player is selling and (finalPrice > initialOffer):

   skillGain *= int(100 * (finalPrice - initialOffer) / finalPrice)

elif player is buying and (finalPrice < initialOffer):

   skillGain *= int(100 * (initialOffer - finalPrice) / initialOffer)

else:

   skillGain = 0

</syntaxhighlight>

All other actions <syntaxhighlight lang="python"> skillGain = skill.gainFactor[action] </syntaxhighlight>

Testing for skill increase <syntaxhighlight lang="python"> progress[skill] += skillGain progressRequirement = 1 + playerSkills[skill]

if skill in player.majorSkills:

   progressRequirement *= fMajorSkillBonus

elif skill in player.minorSkills:

   progressRequirement *= fMinorSkillBonus

elif skill in player.miscSkills:

   progressRequirement *= fMiscSkillBonus

if skill in player.class.specialization.skills:

   progressRequirement *= fSpecialSkillBonus

if int(progress[skill]) >= int(progressRequirement):

   progress[skill] = 0
   playerSkills[skill] increased by 1, triggering further functions

</syntaxhighlight>


Comments[edit]

The progress is preserved, but not the progressRequirement, when the skill is modified by a script function. The skill tooltip always shows the skill progression as a bar labelled with x/100.

Player level progress[edit]

Actions affected On skill level-up
Description
Implementation status Implemented
Analysis status Verified

On new game <syntaxhighlight lang="python"> totalIncreases = 0 # global counter of skill increases attribCounter[8] = [0,0,0,0,0,0,0,0] # counter of attribute bonuses </syntaxhighlight>

On skill level-up <syntaxhighlight lang="python"> if skill in player.majorSkills:

   totalIncreases += iLevelUpMajorMult
   attribCounter[skill->basicAttribute] += iLevelUpMajorMultAttribute

elif skill in player.minorSkills:

   totalIncreases += iLevelUpMinorMult
   attribCounter[skill->basicAttribute] += iLevelUpMinorMultAttribute

elif skill in player.miscSkills:

   attribCounter[skill->basicAttribute] += iLevelupMiscMultAttriubte    # note game setting name has a typo

if totalIncreases >= iLevelUpTotal:

   level up becomes available
   totalIncreases -= iLevelUpTotal    # note rollover mechanic
   attribCounter[8] = [0,0,0,0,0,0,0,0]

</syntaxhighlight>

Levelling up[edit]

Actions affected On resting when a level up is available
Description
Implementation status Implemented
Analysis status Verified

On level up, the player can select up to 3 attributes to improve. Less than 3 may be offered if there are less than 3 attributes that can be improved (attribute < 100). Attributes already >= 100 cannot be selected.

<syntaxhighlight lang="python"> for each attribute:

   count = attribCounter[attribute]
   if count == 0:
       bonus = 1
   elif count <= 9:
       bonus = {iLevelUp01Mult, .. iLevelUp09Mult} selected by count
   else: # count >= 10
       bonus = iLevelUp10Mult
   bonus = min(bonus, 100 - player.attribute[selectedAttribute])

</syntaxhighlight>

NPC Auto-calculate Stats[edit]

Actions affected On creating an NPC flagged with auto-calculate
Description NPCs' auto-calculated stats. Affected by race, class, faction and rank.
Implementation status Implemented
Analysis status Verified


Attributes[edit]

<syntaxhighlight lang="python"> for each attribute: base = race base attribute (+ 10 if a class primary attribute)

k = 0 for each skill with this governing attribute:

   if skill is class major: k += 1
   if skill is class minor: k += 0.5
   if skill is miscellaneous: k += 0.2

final attribute = base + k * (level - 1) round attribute to nearest, half to nearest even (standard IEEE 754 rounding mode) </syntaxhighlight>


Health[edit]

<syntaxhighlight lang="python"> mult = 3

    + 2 if class specialization is combat
    + 1 if class specialization is stealth
    + 1 if endurance is a primary attribute

health = floor(0.5 * (strength + endurance) + mult * (level - 1)) </syntaxhighlight>


Skills[edit]

<syntaxhighlight lang="python"> for each skill:

   if skill is class major: base = 30, k = 1
   if skill is class minor: base = 15, k = 1
   if skill is miscellaneous: base = 5, k = 0.1

   if skill is in class specialization: base += 5, k += 0.5
   if skill has race bonus: base += racebonus
   final skill = base + k * (level - 1)
   round skill to nearest, half to nearest even (standard IEEE 754 rounding mode)

</syntaxhighlight>


Reputation[edit]

<syntaxhighlight lang="python"> if not in a faction:

   reputation = 0

else:

   reputation = iAutoRepFacMod * rank + iAutoRepLevMod * (level - 1)
   where the entry level rank in the faction means rank = 1

</syntaxhighlight>


Comments[edit]

The correct rounding mode is critical for accurate skills, which affect important gameplay like spell auto-selection and training.

Note that there are level 0 NPCs in the game, so the (level - 1) term will become -1; use a signed representation of level in calculations.

NPC Auto-calculate Spells[edit]

Actions affected On creating an NPC flagged with auto-calculate
Description
Implementation status Implemented
Analysis status Verified

Common functions[edit]

<syntaxhighlight lang="python"> function calcWeakestSchool :: (spell, actor) -> (effectiveSchool, skillTerm)

minChance = FLOAT_MAX for each effect in spell:

   x = effect.duration
   if not effect.magicEffect.flags & UNCAPPED_DAMAGE: x = max(1, x)
   x *= 0.1 * effect.magicEffect.baseMagickaCost
   x *= 0.5 * (effect.magnitudeMin + effect.magnitudeMax)
   x += effect.radius * 0.05 * effect.magicEffect.baseMagickaCost
   if effect.rangeType & CAST_TARGET: x *= 1.5
   x *= fEffectCostMult
   s = 2 * actor.skill[effect.magicEffect.school.associatedSkillId]
   if (s - x) < minChance:
       minChance = s - x
       effectiveSchool = effect.magicEffect.school
       skillTerm = s

return effectiveSchool, skillTerm </syntaxhighlight> <syntaxhighlight lang="python"> function calcAutoCastChance :: (spell, actor, effectiveSchool) -> castChance

if spell.castingType != spell: return 100 if spell is flagged always succeeds: return 100

if effectiveSchool != none:

   skillTerm = 2 * actor.skill[effectiveSchool.associatedSkillId]

else:

   _, skillTerm = calcWeakestSchool(spell, actor)

castChance = skillTerm - spell.cost + 0.2 * actor.willpower + 0.1 * actor.luck return castChance </syntaxhighlight>

NPC spells[edit]

<syntaxhighlight lang="python"> baseActor.spells = vector() baseMagicka = fNPCbaseMagickaMult * baseActor.intelligence

spellSchools = { Alteration, Conjuration, Destruction, Illusion, Mysticism, Restoration } schoolCaps = {} # could be an array indexed by school enum

for each school in spellSchools:

   schoolCaps[school] = { count : 0,
                          limit : iAutoSpell{school}Max,
                          reachedLimit : iAutoSpell{school}Max <= 0,
                          minCost : INT_MAX,
                          weakestSpell : none }

for each spell in the game: # note: iteration order is important, see comments

   if spell.isMarkedDeleted: continue
   if spell.castingType != spell: continue
   if not spell.isAutoCalculate: continue
   if baseMagicka < iAutoSpellTimesCanCast * spell.cost: continue
   if spell is in baseActor.race.racialSpells: continue
   
   failedAttrSkillCheck = false
   for each effect in spell:
       if (effect.baseEffect.flags & TARGET_SKILL) and baseActor.skills[effect.targetSkill] < iAutoSpellAttSkillMin:
           failedAttrSkillCheck = true
           break
       if (effect.baseEffect.flags & TARGET_ATTR) and baseActor.attribute[effect.targetAttr] < iAutoSpellAttSkillMin:
           failedAttrSkillCheck = true
           break
   if failedAttrSkillCheck: continue
   
   school, _ = calcWeakestSchool(spell, actor)
   cap = schoolCaps[school]
   if cap.reachedLimit and spell.cost <= cap.minCost: continue
   if calcBaseCastChance(baseActor, spell, school) < fAutoSpellChance: continue
   
   baseActor.spells.add(spell)
   
   if cap.reachedLimit:
       baseActor.spells.remove(cap.weakestSpell)
       cap.weakestSpell = baseActor.spells.findMinCostSpell() # note: not school specific
       cap.minCost = cap.weakestSpell.cost
   else:
       cap.count += 1
       if cap.count == cap.limit:
           cap.reachedLimit = true
           
       if spell.cost < cap.minCost:
           cap.weakestSpell = spell
           cap.minCost = spell.cost

</syntaxhighlight>


Comments[edit]

Auto-calculated spells are selected at initial loading time. baseActor refers to the actor with attributes as loaded or auto-calculated, without any kind of spell effects (i.e. abilities) applied.

Due to poor design, iteration order is critical to the algorithm's behaviour. The list of all spells in the game must be in the same order as loaded from the files. NPC spells are held in a vector, which the findMinCostSpell function scans linearly; during the scan, if there is another spell that matches the lowest cost, it keeps the first one found.

Note that when a spell school is past its limit, the weakest spell is removed, and a new weakest spell is selected. This may not be a spell from the same school as the limit. While this is undesired behaviour, fixing it is likely to cause a major difference from vanilla spell selection, which will not have been play tested. It's not recommend to fix this part at the moment.

PC Starting Spells[edit]

Actions affected On reviewing player stats, after race, class and sign are selected
Description Uses common functions from NPC auto-calc spells. baseActor is the player.
Implementation status Implemented
Analysis status Verified

<syntaxhighlight lang="python"> baseMagicka = fPCbaseMagickaMult * baseActor.intelligence reachedLimit = false weakestSpell = none minCost = INT_MAX

baseActor.spells = vector()

for each spell in the game: # note: iteration order is important, see comments

   if spell.isMarkedDeleted: continue
   if spell.castingType != spell: continue
   if not spell.isPCStartSpell: continue
   if reachedLimit and spell.cost <= minCost: continue
   if spell is in baseActor.spells: continue
   if spell is in baseActor.race.racialSpells: continue
   if baseMagicka < spell.cost: continue
   if calcAutoCastChance(spell, baseActor, none) < fAutoPCSpellChance: continue
   failedAttrSkillCheck = false
   for each effect in spell:
       if (effect.baseEffect.flags & TARGET_SKILL) and baseActor.skills[effect.targetSkill] < iAutoSpellAttSkillMin:
           failedAttrSkillCheck = true
           break
       if (effect.baseEffect.flags & TARGET_ATTR) and baseActor.attribute[effect.targetAttr] < iAutoSpellAttSkillMin:
           failedAttrSkillCheck = true
           break
   if failedAttrSkillCheck: continue
       
   baseActor.spells.add(spell)
   
   if reachedLimit:
       baseActor.spells.remove(weakestSpell)
       weakestSpell = baseActor.spells.findMinCostSpell()
       minCost = weakestSpell.cost
   else:
       if spell.cost < minCost:
           weakestSpell = spell
           minCost = spell.cost
       if baseActor.spells.size() == iAutoPCSpellMax:
           reachedLimit = true

</syntaxhighlight>


Comments[edit]

This is first executed once the stat review menu appear, and then executed every time the player modifies their character. The spell list must therefore be cleared before selecting new spells.

See NPC auto-calculated spells for additional comments.



{{#switch:|subgroup|child=|none=|#default=

}}