Research:Stats and Levelling: Difference between revisions
(Added auto-calculated spells.) |
|||
Line 154: | Line 154: | ||
: To reset the progress, level first to the next level and then use player->setSkill to the target level. | : To reset the progress, level first to the next level and then use player->setSkill to the target level. | ||
==NPC Auto-calculate | ==NPC Auto-calculate Stats== | ||
{{Formula | {{Formula | ||
|On creating an NPC | |On creating an NPC flagged with auto-calculate | ||
|NPCs' auto-calculated stats. Affected by race, class, faction and rank. | |NPCs' auto-calculated stats. Affected by race, class, faction and rank. | ||
|{{StatusCol|green|Implemented}} | |{{StatusCol|green|Implemented}} | ||
Line 217: | Line 217: | ||
===Spells=== | ==NPC Auto-calculate Spells== | ||
{{Formula | |||
|On creating an NPC flagged with auto-calculate | |||
| | |||
| | |||
|{{StatusCol|green|Verified}}}} | |||
===Common functions=== | |||
<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 += spell.radius * 0.05 * effect.magicEffect.baseMagickaCost | |||
if effect.flags & 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 - spellCost + 0.2 * actorWillpower + 0.1 * actorLuck) * fatigueTerm | |||
return castChance | |||
</syntaxhighlight> | |||
===NPC spells=== | |||
<syntaxhighlight lang="python"> | |||
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.type != SPELL: continue | |||
if not spell.isAutoCalculate: continue | |||
if baseMagicka < iAutoSpellTimesCanCast * spell.cost: continue | |||
if spell is in baseActor.race.racialSpells: continue | |||
for each effect in spell: | |||
if (effect.baseEffect.flags & TARGET_SKILL) and baseActor.skills[effect.targetSkill] < iAutoSpellAttSkillMin: continue | |||
if (effect.baseEffect.flags & TARGET_ATTR) and baseActor.attribute[effect.targetAttr] < iAutoSpellAttSkillMin: 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> | |||
==PC Starting Spells== | |||
{{Formula | |||
|On reviewing player stats, after race, class and sign are selected | |||
|Uses common functions from NPC auto-calc spells. | |||
| | |||
|{{StatusCol|green|Verified}}}} | |||
<syntaxhighlight lang="python"> | |||
baseMagicka = fPCbaseMagickaMult * baseActor.intelligence | |||
reachedLimit = false | |||
weakestSpell = none | |||
minCost = INT_MAX | |||
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 | |||
for each effect in spell: | |||
if (effect.baseEffect.flags & TARGET_SKILL) and baseActor.skills[effect.targetSkill] < iAutoSpellAttSkillMin: continue | |||
if (effect.baseEffect.flags & TARGET_ATTR) and baseActor.attribute[effect.targetAttr] < iAutoSpellAttSkillMin: 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> |
Revision as of 22:33, 4 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, spells incomplete |
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 += spell.radius * 0.05 * effect.magicEffect.baseMagickaCost
if effect.flags & 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 - spellCost + 0.2 * actorWillpower + 0.1 * actorLuck) * fatigueTerm
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.type != SPELL: continue
if not spell.isAutoCalculate: continue
if baseMagicka < iAutoSpellTimesCanCast * spell.cost: continue
if spell is in baseActor.race.racialSpells: continue
for each effect in spell:
if (effect.baseEffect.flags & TARGET_SKILL) and baseActor.skills[effect.targetSkill] < iAutoSpellAttSkillMin: continue
if (effect.baseEffect.flags & TARGET_ATTR) and baseActor.attribute[effect.targetAttr] < iAutoSpellAttSkillMin: 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
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. |
Implementation status | |
Analysis status | Verified |
baseMagicka = fPCbaseMagickaMult * baseActor.intelligence
reachedLimit = false
weakestSpell = none
minCost = INT_MAX
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
for each effect in spell:
if (effect.baseEffect.flags & TARGET_SKILL) and baseActor.skills[effect.targetSkill] < iAutoSpellAttSkillMin: continue
if (effect.baseEffect.flags & TARGET_ATTR) and baseActor.attribute[effect.targetAttr] < iAutoSpellAttSkillMin: 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