Research:Stats and Levelling: Difference between revisions

From OpenMW Wiki
Jump to navigation Jump to search
(Auto-calc spells, corrections.)
(→‎Common functions: Effect radius and cast range correction.)
Line 232: Line 232:
     x *= 0.1 * effect.magicEffect.baseMagickaCost
     x *= 0.1 * effect.magicEffect.baseMagickaCost
     x *= 0.5 * (effect.magnitudeMin + effect.magnitudeMax)
     x *= 0.5 * (effect.magnitudeMin + effect.magnitudeMax)
     x += spell.radius * 0.05 * effect.magicEffect.baseMagickaCost
     x += effect.radius * 0.05 * effect.magicEffect.baseMagickaCost
     if effect.flags & CAST_TARGET: x *= 1.5
     if effect.rangeType & CAST_TARGET: x *= 1.5
     x *= fEffectCostMult
     x *= fEffectCostMult


Line 258: Line 258:
return castChance
return castChance
</syntaxhighlight>
</syntaxhighlight>


===NPC spells===
===NPC spells===

Revision as of 16:46, 11 July 2014

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

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.