Research:Stats and Levelling
PC & NPC dynamic stats
Actions affected | Recalculated when base attributes are modified |
Description | Health points, magicka, fatigue, and encumbrance |
Implementation status | partially done (magicka needs adjustment, changed to current values not implemented yet) |
Analysis status | Ignores all magicka multiplier GMSTs, ignores differences between PC and NPC calculations |
HP
Initial HP
health = int(0.5 * (baseStrength + baseEndurance))
At every level-up
occurs after attributes have been increased by the level-up
bonusHP = fLevelUpHealthEndMult * baseEndurance
Magicka points
Every time intelligence or magicka multiplier is modified
M = total magic bonus from race, item, sign (Maximum Magicka Multiplier and Stunted Magicka effects) and magicka GMSTs
maxMagicka = M * intelligence
currentMagicka is rescaled to preserve % left
Fatigue
Every time a base stat is modified
maxFatigue = strength + willpower + agility + endurance
currentFatigue is rescaled to preserve % left
Encumbrance
Every time strength is modified
maxEncumbrance = fEncumbranceStrMult * strength
Resting
Actions affected | On resting/waiting |
Description | Waiting also applies to time passed after training. Resting also applies to time passed during travel. |
Implementation status | implemented, bug corrected |
Analysis status | Verified, but sleep interruption has bugs |
if resting in an exterior cell and the region has a sleep creature levelled 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
sleep will be interrupted with 1 creature from the region levelled list
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
Comments
Resting allows all actors in the game to recover. There is a bug with interrupted sleep; the code only spawns the first creature it finds in the levelled list, as well as calculating the number of creatures incorrectly. interruptingCreatures should be 1 + roll iNumberCreatures, and that number of creatures should be spawned.
Player levelling
PC skill progress
Actions affected | On skill level-up |
Description | |
Implementation status | Implemented |
Analysis status | Usable, doesn't cover how all of the counters wrap on level up |
When player receive point in the skill:
int total # global counter of skill increases
int attribCounter[8] # counter of attribute bonuses
if skill in Major:
total += iLevelUpMajorMult
attribCounter [skill->basicAttribute] += iLevelUpMajorMultAttribute
if skill in Minor:
total += iLevelUpMinorMult
attribCounter [skill->basicAttribute] += iLevelUpMinorMultAttribute
if skill in Misc:
attribCounter [skill->basicAttribute] += iLevelupMiscMultAttriubte # note: game setting name has a typo
if total >= iLevelUpTotal:
level up
Levelling up
Actions affected | On resting when a level up is available |
Description | |
Implementation status | Implemented |
Analysis status | Requires documentation of level-up conditions and GMSTs |
On level-up PC get 3 points to redistribute, bonus will be this:
if attribCounter != 0:
bonus = iLevelUp$$Mult
else:
bonus = 1
where $$ is value of attribute counter
Skill increases
Actions affected | On exercising a skill |
Description | |
Implementation status | Implemented |
Analysis status | Accurate, but doesn't cover how progress wraps on level up and multiple level ups per action |
Based on research on leveling of the Alchemy skill (ref: http://openmw.org/forum/viewtopic.php?f=2&t=853)
level_progress = 1 / ((level + 1) * (1/skill_gain_factor) * skill_type_GMST * specialisation_bonus)
level_progress = the progress factor through a level (from 0 to 1).
level = current level of the skill.
skill_gain_factor = skill gain factor(s) that come from the skill records.
- For alchemy only "Potion creation" (default value: 2.00) is used.
- For other skills, please add alphabetically.
skill_type_GMST = value of the GMST corresponding skill type:
- Major skill: GMST fMajorSkillBonus (default: 0.75)
- Minor skill: GMST fMinorSkillBonus (default: 1.00)
- Misc skill: GMST fMiscSkillBonus (default: 1.25)
specialisation_bonus = value of fSpecialSkillBonus (default: 0.80) when the Player has the same specialization as the skill or 1.00 if not.
On using player->setSkill
- Experiments on Alchemy showed that using player->setAlchemy did change the alchemy level, but not the skill progress. Therefore, when lowering the alchemy level, the skill progress will be recalculated for that level en give a higher progress. When increasing the alchemy level, the reverse will happen, resulting in a lower skill progress.
- To reset the progress, level first to the next level and then use player->setSkill to the target level.
NPC Auto-calculate Stats
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
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)
Health
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))
Skills
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)
Reputation
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
Comments
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
Actions affected | On creating an NPC flagged with auto-calculate |
Description | |
Implementation status | |
Analysis status | Verified |
Common functions
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
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
NPC spells
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:
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
Comments
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.
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
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 | |
Analysis status | Verified |
baseMagicka = fPCbaseMagickaMult * baseActor.intelligence
reachedLimit = false
weakestSpell = none
minCost = INT_MAX
baseActor.spells.clear()
for each spell in the game:
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
Comments
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.