Level up mod

General discussion regarding the OpenMW project.
For technical support, please use the Support subforum.
Post Reply
ezze
Posts: 513
Joined: 21 Nov 2013, 13:20

Level up mod

Post by ezze »

It is long time I was active (as forum poster) here, recently I wanted to play Morrowind once more.

I still hate the leveling system and I wanted to see if it were possible to do a minimalist mod to fix it.


The main problem I have with the default level up system is that it pushes the player to seek for easy challenges, like to cast the same weak spell continuously.

As minimal change I'd just want to push the player to look for challenges of its level, not too easy, not too hard.
The base idea would be that being x the chance of success (so x is in [0, 1]) the skill gain is proportional to x on failure and on 1-x on success.
I am aware that some skills (like Acrobatics) have no concept of "failing," but I have an idea for those too.


To see if it were possible to create such a mod in OpenMW I checked the code a bit.

At the moment the engine (a part of Speechcraft) does not really consider failures to increase the skills, and this is a necessary feature to implement the mod.

The member function MWClass::Npc::skillUsageSucceeded is called each time the player succeeds, the function will call MWMechanics::NpcStats ::useSkill to update the skills. Besides, there is a parameter extraFactor that allows to scale how much skill gain the player gets.
The default values about how much successes increase are outside the engine and stored in the SKIL records.


The problem is then: I see no ways to make the mod I'd like without changing the engine.

Here is a possible solution. I am writing this message because I'd like to ask if it would acceptable, this solution would implies some changes in the engine. But of course, by default the behavior will be the same.

- Add a new function similar to skillUsageSucceeded called on failure.
- Look for a new, or unused, GMST (or a similar flag) that, if present, sets up the new weights that are passed to extraFactor; if the new GMST is not there the value stays as now: 1. for everything a part of Mercantile.

I am fairly sure I'd be able to do so, but it would be an acceptable patch? I remember logical deviations from the original engine were frowned upon, but it was years ago and now apparently the version is "not 1.0" because of the editor more than the game. So perhaps it has changed.


Of course, given the problem other solutions are welcome. If I am missing something and it is actually possible to do such a mod without altering the engine code by all means suggest how; however I do not intend to do dumb hacks as I was forced with the original engine.
ezze
Posts: 513
Joined: 21 Nov 2013, 13:20

Re: Level up mod

Post by ezze »

Checking the code better there is no reason do duplicate MWClass::Npc::skillUsageSucceeded or touch GMSTs.

It is better to simply call MWClass::Npc::skillUsageSucceeded with a different extraFactor and similarly to some MCP changes (e.g., can loot during death animation) add the option to the game settings.
ezze
Posts: 513
Joined: 21 Nov 2013, 13:20

Re: Level up mod

Post by ezze »

Here is how it will look like. For now only on one of easiest skill: Block. I don't expect the others to be much different, though.

Code: Select all

diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp
index 183845b..6ea956a 100644
--- a/apps/openmw/mwmechanics/combat.cpp
+++ b/apps/openmw/mwmechanics/combat.cpp
@@ -56,6 +56,8 @@ namespace MWMechanics
 
     bool blockMeleeAttack(const MWWorld::Ptr &attacker, const MWWorld::Ptr &blocker, const MWWorld::Ptr &weapon, float damage, float attackStrength)
     {
+        static bool increaseOnFailure = Settings::Manager::getBool("increase skill on failure", "Game");
+
         if (!blocker.getClass().hasInventoryStore(blocker))
             return false;
 
@@ -114,6 +116,7 @@ namespace MWMechanics
         int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger();
         int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger();
         x = std::min(iBlockMaxChance, std::max(iBlockMinChance, x));
+        float block_chance = x / 100.f;
 
         if (Misc::Rng::roll0to99() < x)
         {
@@ -139,11 +142,20 @@ namespace MWMechanics
 
             blockerStats.setBlock(true);
 
-            if (blocker == getPlayer())
-                blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0);
+            if (blocker == getPlayer()) {
+                if (increaseOnFailure) {
+                    blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0, 1.f - block_chance);
+                } else {
+                    blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0);
+                }
+            }
 
             return true;
         }
+
+        if (increaseOnFailure && blocker == getPlayer())
+            blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0, block_chance);
+
         return false;
     }

diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp
index 0c00d3b..180966a 100644
--- a/apps/openmw/mwclass/npc.cpp
+++ b/apps/openmw/mwclass/npc.cpp
@@ -1110,6 +1110,8 @@ namespace MWClass
 
     void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const
     {
+        if (extraFactor <= 0.0f) return;
+
         MWMechanics::NpcStats& stats = getNpcStats (ptr);
 
         if (stats.isWerewolf())

diff --git a/files/settings-default.cfg b/files/settings-default.cfg
index ac5433f..b01b884 100644
--- a/files/settings-default.cfg
+++ b/files/settings-default.cfg
@@ -337,6 +337,9 @@ trainers training skills based on base skill = false
 # Make stealing items from NPCs that were knocked down possible during combat.
 always allow stealing from knocked out actors = false
 
+# Increase skills also on failure weighting in the difficulty
+increase skill on failure = false
+
 [General]
 
 # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16).
User avatar
Amenophis
Posts: 320
Joined: 30 Oct 2011, 04:34
Location: Fortaleza - Ceará - Brasil

Re: Level up mod

Post by Amenophis »

You could do a MR on the OpenMW's GITLAB and call for discussion here.
ezze
Posts: 513
Joined: 21 Nov 2013, 13:20

Re: Level up mod

Post by ezze »

I am afraid it is out of scope, but perhaps I will.

Here is the version 0, it does not comprise all abilities, but includes the most important ones.

If anyone can try it out to see "how it feels", feedback would be greatly appreciated.

Code: Select all

diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp
index 0c00d3b..7e41556 100644
--- a/apps/openmw/mwclass/npc.cpp
+++ b/apps/openmw/mwclass/npc.cpp
@@ -541,6 +541,8 @@ namespace MWClass
 
     void Npc::hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const
     {
+        static bool increaseOnFailure = Settings::Manager::getBool("increase skill on failure", "Game");
+
         MWBase::World *world = MWBase::Environment::get().getWorld();
 
         const MWWorld::Store<ESM::GameSetting> &store = world->getStore().get<ESM::GameSetting>();
@@ -589,6 +591,10 @@ namespace MWClass
 
         if (Misc::Rng::roll0to99() >= hitchance)
         {
+            if(increaseOnFailure && ptr == MWMechanics::getPlayer()) {
+                skillUsageSucceeded(ptr, weapskill, 0, hitchance / 100.f);
+            }
+
             othercls.onHit(victim, 0.0f, false, weapon, ptr, osg::Vec3f(), false);
             MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr);
             return;
@@ -621,7 +627,11 @@ namespace MWClass
         }
         if(ptr == MWMechanics::getPlayer())
         {
-            skillUsageSucceeded(ptr, weapskill, 0);
+            if (increaseOnFailure)
+                skillUsageSucceeded(ptr, weapskill, 0, 1.f - hitchance / 100.f);
+            else
+                skillUsageSucceeded(ptr, weapskill, 0);
+
 
             const MWMechanics::AiSequence& seq = victim.getClass().getCreatureStats(victim).getAiSequence();
 
@@ -1110,6 +1120,8 @@ namespace MWClass
 
     void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const
     {
+        if (extraFactor <= 0.0f) return;
+
         MWMechanics::NpcStats& stats = getNpcStats (ptr);
 
         if (stats.isWerewolf())
diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp
index 183845b..32781ab 100644
--- a/apps/openmw/mwmechanics/combat.cpp
+++ b/apps/openmw/mwmechanics/combat.cpp
@@ -56,6 +56,8 @@ namespace MWMechanics
 
     bool blockMeleeAttack(const MWWorld::Ptr &attacker, const MWWorld::Ptr &blocker, const MWWorld::Ptr &weapon, float damage, float attackStrength)
     {
+        static bool increaseOnFailure = Settings::Manager::getBool("increase skill on failure", "Game");
+
         if (!blocker.getClass().hasInventoryStore(blocker))
             return false;
 
@@ -114,6 +116,7 @@ namespace MWMechanics
         int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger();
         int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger();
         x = std::min(iBlockMaxChance, std::max(iBlockMinChance, x));
+        float block_chance = x / 100.f;
 
         if (Misc::Rng::roll0to99() < x)
         {
@@ -139,11 +142,20 @@ namespace MWMechanics
 
             blockerStats.setBlock(true);
 
-            if (blocker == getPlayer())
-                blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0);
+            if (blocker == getPlayer()) {
+                if (increaseOnFailure) {
+                    blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0, 1.f - block_chance);
+                } else {
+                    blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0);
+                }
+            }
 
             return true;
         }
+
+        if (increaseOnFailure && blocker == getPlayer())
+            blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0, block_chance);
+
         return false;
     }
 
@@ -193,6 +205,8 @@ namespace MWMechanics
     void projectileHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWWorld::Ptr weapon, const MWWorld::Ptr& projectile,
                        const osg::Vec3f& hitPosition, float attackStrength)
     {
+        static bool increaseOnFailure = Settings::Manager::getBool("increase skill on failure", "Game");
+
         MWBase::World *world = MWBase::Environment::get().getWorld();
         const MWWorld::Store<ESM::GameSetting> &gmst = world->getStore().get<ESM::GameSetting>();
 
@@ -210,8 +224,13 @@ namespace MWMechanics
 
             int skillValue = attacker.getClass().getSkill(attacker, weapon.getClass().getEquipmentSkill(weapon));
 
-            if (Misc::Rng::roll0to99() >= getHitChance(attacker, victim, skillValue))
+            float hitChance = getHitChance(attacker, victim, skillValue);
+
+            if (Misc::Rng::roll0to99() >= hitChance)
             {
+                if (increaseOnFailure && attacker == getPlayer())
+                    attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, 0, hitChance / 100.f);
+
                 victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false);
                 MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker);
                 return;
@@ -231,7 +250,12 @@ namespace MWMechanics
             applyWerewolfDamageMult(victim, projectile, damage);
 
             if (attacker == getPlayer())
-                attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, 0);
+            {
+                if (increaseOnFailure)
+                    attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, 0, 1.f - hitChance / 100.f);
+                else
+                    attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, 0);
+            }
 
             const MWMechanics::AiSequence& sequence = victim.getClass().getCreatureStats(victim).getAiSequence();
             bool unaware = attacker == getPlayer() && !sequence.isInCombat()
diff --git a/apps/openmw/mwmechanics/repair.cpp b/apps/openmw/mwmechanics/repair.cpp
index 389d00d..34bdd2e 100644
--- a/apps/openmw/mwmechanics/repair.cpp
+++ b/apps/openmw/mwmechanics/repair.cpp
@@ -1,6 +1,7 @@
 #include "repair.hpp"
 
 #include <components/misc/rng.hpp>
+#include <components/settings/settings.hpp>
 
 #include "../mwbase/world.hpp"
 #include "../mwbase/environment.hpp"
@@ -18,6 +19,8 @@ namespace MWMechanics
 
 void Repair::repair(const MWWorld::Ptr &itemToRepair)
 {
+    static bool increaseOnFailure = Settings::Manager::getBool("increase skill on failure", "Game");
+
     MWWorld::Ptr player = getPlayer();
     MWWorld::LiveCellRef<ESM::Repair> *ref =
         mTool.get<ESM::Repair>();
@@ -41,10 +44,10 @@ void Repair::repair(const MWWorld::Ptr &itemToRepair)
 
     float toolQuality = ref->mBase->mData.mQuality;
 
-    float x = (0.1f * pcStrength + 0.1f * pcLuck + armorerSkill) * fatigueTerm;
+    float successChance = (0.1f * pcStrength + 0.1f * pcLuck + armorerSkill) * fatigueTerm;
 
     int roll = Misc::Rng::roll0to99();
-    if (roll <= x)
+    if (roll <= successChance)
     {
         int y = static_cast<int>(fRepairAmountMult * toolQuality * roll);
         y = std::max(1, y);
@@ -63,13 +66,19 @@ void Repair::repair(const MWWorld::Ptr &itemToRepair)
             stacked->getRefData().getLocals().setVarByInt(script, "onpcrepair", 1);
 
         // increase skill
-        player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, 0);
+        if (increaseOnFailure)
+            player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, 0, 1.f - successChance / 100.f);
+        else
+            player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, 0);
 
         MWBase::Environment::get().getWindowManager()->playSound("Repair");
         MWBase::Environment::get().getWindowManager()->messageBox("#{sRepairSuccess}");
     }
     else
     {
+        if (increaseOnFailure)
+            player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, 0, successChance / 100.f);
+
         MWBase::Environment::get().getWindowManager()->playSound("Repair Fail");
         MWBase::Environment::get().getWindowManager()->messageBox("#{sRepairFailed}");
     }
diff --git a/apps/openmw/mwmechanics/security.cpp b/apps/openmw/mwmechanics/security.cpp
index 001375f..2724a40 100644
--- a/apps/openmw/mwmechanics/security.cpp
+++ b/apps/openmw/mwmechanics/security.cpp
@@ -1,5 +1,6 @@
 #include "security.hpp"
 
+#include <components/settings/settings.hpp>
 #include <components/misc/rng.hpp>
 
 #include "../mwworld/class.hpp"
@@ -28,6 +29,7 @@ namespace MWMechanics
     void Security::pickLock(const MWWorld::Ptr &lock, const MWWorld::Ptr &lockpick,
                             std::string& resultMessage, std::string& resultSound)
     {
+        static bool increaseOnFailure = Settings::Manager::getBool("increase skill on failure", "Game");
         if (lock.getCellRef().getLockLevel() <= 0 ||
             lock.getCellRef().getLockLevel() == ESM::UnbreakableLock ||
             !lock.getClass().hasToolTip(lock)) //If it's unlocked or can not be unlocked back out immediately
@@ -53,12 +55,21 @@ namespace MWMechanics
             if (Misc::Rng::roll0to99() <= x)
             {
                 lock.getCellRef().unlock();
+
                 resultMessage = "#{sLockSuccess}";
                 resultSound = "Open Lock";
-                mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 1);
+
+                if (increaseOnFailure)
+                    mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 1, 1.f - x / 100.f);
+                else
+                    mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 1);
             }
-            else
+            else {
+                if (increaseOnFailure)
+                    mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 1, x / 100.f);
+
                 resultMessage = "#{sLockFail}";
+            }
         }
 
         int uses = lockpick.getClass().getItemHealth(lockpick);
@@ -71,6 +82,7 @@ namespace MWMechanics
     void Security::probeTrap(const MWWorld::Ptr &trap, const MWWorld::Ptr &probe,
                              std::string& resultMessage, std::string& resultSound)
     {
+        static bool increaseOnFailure = Settings::Manager::getBool("increase skill on failure", "Game");
         if (trap.getCellRef().getTrap()  == "")
             return;
 
@@ -98,10 +110,16 @@ namespace MWMechanics
 
                 resultSound = "Disarm Trap";
                 resultMessage = "#{sTrapSuccess}";
-                mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 0);
+                if (increaseOnFailure)
+                    mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 0, 1.f - x / 100.f);
+                else
+                    mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 0);
             }
-            else
+            else {
+                if (increaseOnFailure)
+                    mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 0, x / 100.f);
                 resultMessage = "#{sTrapFail}";
+            }
         }
 
         int uses = probe.getClass().getItemHealth(probe);
diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp
index 9f71082..65cc37e 100644
--- a/apps/openmw/mwmechanics/spellcasting.cpp
+++ b/apps/openmw/mwmechanics/spellcasting.cpp
@@ -2,6 +2,7 @@
 
 #include <components/misc/constants.hpp>
 #include <components/misc/rng.hpp>
+#include <components/settings/settings.hpp>
 
 #include "../mwbase/windowmanager.hpp"
 #include "../mwbase/soundmanager.hpp"
@@ -555,6 +556,8 @@ namespace MWMechanics
 
     bool CastSpell::cast(const ESM::Spell* spell)
     {
+        static bool increaseOnFailure = Settings::Manager::getBool("increase skill on failure", "Game");
+
         mSourceName = spell->mName;
         mId = spell->mId;
         mStack = false;
@@ -564,6 +567,7 @@ namespace MWMechanics
         int school = 0;
 
         bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
+        float successChance = 100.;
 
         if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mManualSpell)
         {
@@ -583,19 +587,17 @@ namespace MWMechanics
                 fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); 
                 stats.setFatigue(fatigue);
 
-                bool fail = false;
-
                 // Check success
-                float successChance = getSpellSuccessChance(spell, mCaster, nullptr, true, false);
+                successChance = getSpellSuccessChance(spell, mCaster, nullptr, true, false);
                 if (Misc::Rng::roll0to99() >= successChance)
                 {
-                    if (mCaster == getPlayer())
+                    if (mCaster == getPlayer()) {
+                        if (increaseOnFailure && !mManualSpell && mCaster == getPlayer() && spellIncreasesSkill(spell))
+                            mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0, successChance / 100.f);
+
                         MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicSkillFail}");
-                    fail = true;
-                }
+                    }
 
-                if (fail)
-                {
                     // Failure sound
                     static const std::string schools[] = {
                         "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
@@ -612,8 +614,13 @@ namespace MWMechanics
                 stats.getSpells().usePower(spell);
         }
 
-        if (!mManualSpell && mCaster == getPlayer() && spellIncreasesSkill(spell))
-            mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0);
+        if (!mManualSpell && mCaster == getPlayer() && spellIncreasesSkill(spell)) {
+            if (increaseOnFailure) {
+                mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0, 1. - successChance / 100.f);
+            } else {
+                mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0);
+            }
+        }
 
         // A non-actor doesn't play its spell cast effects from a character controller, so play them here
         if (!mCaster.getClass().isActor())
diff --git a/files/settings-default.cfg b/files/settings-default.cfg
index ac5433f..b01b884 100644
--- a/files/settings-default.cfg
+++ b/files/settings-default.cfg
@@ -337,6 +337,9 @@ trainers training skills based on base skill = false
 # Make stealing items from NPCs that were knocked down possible during combat.
 always allow stealing from knocked out actors = false
 
+# Increase skills also on failure weighting the difficulty
+increase skill on failure = false
+
 [General]
 
 # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16).
Attachments
plus_three_increase.zip
Changes the SKILL settings to match
(1.99 KiB) Downloaded 186 times
ezze
Posts: 513
Joined: 21 Nov 2013, 13:20

Re: Level up mod

Post by ezze »

I player for few hours a character with this new rules. I tried to play "naturally" not to push the game or maximize.

I have to say I am fairly happy, while it does not change too much for swordplay, it pushes to use more dangerous spells instead of safer ones and with security is really satisfying to see "Your security has increased" after breaking a couple of lockpicks while trying to open a difficult door.


It is not about the mod and it is not a big news, but OpenMW and OpenCS OpenCS-MW did their job really well. So thanks to everyone.
User avatar
Amenophis
Posts: 320
Joined: 30 Oct 2011, 04:34
Location: Fortaleza - Ceará - Brasil

Re: Level up mod

Post by Amenophis »

Maybe you could get more reactions for your branch on the OpenMW Discord channel. I'm intrigued with this mod and I'd like to test it someday.
ezze
Posts: 513
Joined: 21 Nov 2013, 13:20

Re: Level up mod

Post by ezze »

This place seems kinda dead. I guess I'll check there...
ezze
Posts: 513
Joined: 21 Nov 2013, 13:20

Re: Level up mod

Post by ezze »

While I am quite happy of the results with weapons' related abilities. When trying to play a wizard character the results were disappointing.

Spells are expensive and their failure dangerous so one needs reliable spells to go on. But reliable means that you it high chance of success, so it become too difficult to increase.

I'd like to try to put a minimum of how much the spell ability increases based on how much magicka the spell cased uses. Something like: a spell that uses 1/5 of your magicka will give you the normal increase as minimum. The 1/5 is a minimum, it can be more using the previous formula.

In formula, being x the chance of success (so x is in [0, 1]) the skill gain is proportional to x on failure and on 1-x on success, but the minimum is how much magicka it uses.
Post Reply