diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index 57886d5399..115416cd8c 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -62,6 +62,7 @@ namespace MWBase virtual void objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; virtual void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) = 0; virtual void animationTextKey(const MWWorld::Ptr& actor, const std::string& key) = 0; + virtual void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) = 0; virtual void playAnimation(const MWWorld::Ptr& object, const std::string& groupname, const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, std::string_view start, std::string_view stop, float startpoint, uint32_t loops, bool loopfallback) diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index de587954b8..7eda9cd2cf 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -22,6 +22,7 @@ #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -655,7 +656,7 @@ namespace MWClass ESM::RefId weapskill = ESM::Skill::HandToHand; if (!weapon.isEmpty()) weapskill = weapon.getClass().getEquipmentSkill(weapon); - skillUsageSucceeded(ptr, weapskill, 0); + skillUsageSucceeded(ptr, weapskill, ESM::Skill::Weapon_SuccessfulHit); const MWMechanics::AiSequence& seq = victim.getClass().getCreatureStats(victim).getAiSequence(); @@ -845,7 +846,7 @@ namespace MWClass ESM::RefId skill = armor.getClass().getEquipmentSkill(armor); if (ptr == MWMechanics::getPlayer()) - skillUsageSucceeded(ptr, skill, 0); + skillUsageSucceeded(ptr, skill, ESM::Skill::Armor_HitByOpponent); if (skill == ESM::Skill::LightArmor) sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f); @@ -855,7 +856,7 @@ namespace MWClass sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f); } else if (ptr == MWMechanics::getPlayer()) - skillUsageSucceeded(ptr, ESM::Skill::Unarmored, 0); + skillUsageSucceeded(ptr, ESM::Skill::Unarmored, ESM::Skill::Armor_HitByOpponent); } } @@ -1131,16 +1132,7 @@ namespace MWClass void Npc::skillUsageSucceeded(const MWWorld::Ptr& ptr, ESM::RefId skill, int usageType, float extraFactor) const { - MWMechanics::NpcStats& stats = getNpcStats(ptr); - - if (stats.isWerewolf()) - return; - - MWWorld::LiveCellRef* ref = ptr.get(); - - const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(ref->mBase->mClass); - - stats.useSkill(skill, *class_, usageType, extraFactor); + MWBase::Environment::get().getLuaManager()->skillUse(ptr, skill, usageType, extraFactor); } float Npc::getArmorRating(const MWWorld::Ptr& ptr) const diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 2b7774d8c9..3b0ba47250 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -543,7 +543,8 @@ namespace MWDialogue mPermanentDispositionChange += perm; MWWorld::Ptr player = MWMechanics::getPlayer(); - player.getClass().skillUsageSucceeded(player, ESM::Skill::Speechcraft, success ? 0 : 1); + player.getClass().skillUsageSucceeded( + player, ESM::Skill::Speechcraft, success ? ESM::Skill::Speechcraft_Success : ESM::Skill::Speechcraft_Fail); if (success) { diff --git a/apps/openmw/mwgui/pickpocketitemmodel.cpp b/apps/openmw/mwgui/pickpocketitemmodel.cpp index bfaf011835..fa7bce449b 100644 --- a/apps/openmw/mwgui/pickpocketitemmodel.cpp +++ b/apps/openmw/mwgui/pickpocketitemmodel.cpp @@ -134,7 +134,7 @@ namespace MWGui return false; } else - player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 1); + player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, ESM::Skill::Sneak_PickPocket); return true; } diff --git a/apps/openmw/mwlua/engineevents.cpp b/apps/openmw/mwlua/engineevents.cpp index 43507ff1a5..7cc2b3db48 100644 --- a/apps/openmw/mwlua/engineevents.cpp +++ b/apps/openmw/mwlua/engineevents.cpp @@ -94,6 +94,15 @@ namespace MWLua if (auto* scripts = getLocalScripts(actor)) scripts->onAnimationTextKey(event.mGroupname, event.mKey); } + + void operator()(const OnSkillUse& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onSkillUse(event.mSkill, event.useType, event.scale); + } private: MWWorld::Ptr getPtr(ESM::RefNum id) const diff --git a/apps/openmw/mwlua/engineevents.hpp b/apps/openmw/mwlua/engineevents.hpp index bf8d219fd5..b9f4c25b39 100644 --- a/apps/openmw/mwlua/engineevents.hpp +++ b/apps/openmw/mwlua/engineevents.hpp @@ -57,8 +57,21 @@ namespace MWLua std::string mGroupname; std::string mKey; }; + struct OnAnimationTextKey + { + ESM::RefNum mActor; + std::string mGroupname; + std::string mKey; + }; + struct OnSkillUse + { + ESM::RefNum mActor; + std::string mSkill; + int useType; + float scale; + }; using Event = std::variant; + OnAnimationTextKey, OnSkillUse>; void clear() { mQueue.clear(); } void addToQueue(Event e) { mQueue.push_back(std::move(e)); } diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 1d5e710869..0c0af5b902 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -176,7 +176,7 @@ namespace MWLua { this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); registerEngineHandlers({ &mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers, &mOnActivatedHandlers, - &mOnTeleportedHandlers, &mOnAnimationTextKeyHandlers, &mOnPlayAnimationHandlers }); + &mOnTeleportedHandlers, &mOnAnimationTextKeyHandlers, &mOnPlayAnimationHandlers, &mOnSkillUse }); } void LocalScripts::setActive(bool active) diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index 230ec93d3c..f5f740b1c6 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -79,6 +79,10 @@ namespace MWLua { callEngineHandlers(mOnPlayAnimationHandlers, groupname, options); } + void onSkillUse(std::string_view skillId, int useType, float scale) + { + callEngineHandlers(mOnSkillUse, skillId, useType, scale); + } void applyStatsCache(); @@ -93,6 +97,7 @@ namespace MWLua EngineHandlerList mOnTeleportedHandlers{ "onTeleported" }; EngineHandlerList mOnAnimationTextKeyHandlers{ "_onAnimationTextKey" }; EngineHandlerList mOnPlayAnimationHandlers{ "_onPlayAnimation" }; + EngineHandlerList mOnSkillUse{ "_onSkillUse" }; }; } diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index ac5ae60f7d..0974856d25 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -406,6 +406,11 @@ namespace MWLua scripts->onPlayAnimation(groupname, options); } + void LuaManager::skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) + { + mEngineEvents.addToQueue(EngineEvents::OnSkillUse{ getId(actor), skillId.serializeText(), useType, scale }); + } + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) { mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index a539d04348..2e29ff272a 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -84,6 +84,7 @@ namespace MWLua const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, std::string_view start, std::string_view stop, float startpoint, uint32_t loops, bool loopfallback) override; + void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) override; void exteriorCreated(MWWorld::CellStore& cell) override { mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell }); diff --git a/apps/openmw/mwlua/stats.cpp b/apps/openmw/mwlua/stats.cpp index 02bed00bf5..224fdba0f2 100644 --- a/apps/openmw/mwlua/stats.cpp +++ b/apps/openmw/mwlua/stats.cpp @@ -73,6 +73,94 @@ namespace MWLua "StatUpdateAction"); } + static void setCreatureValue(Index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + if (prop == "current") + stats.setLevel(LuaUtil::cast(value)); + } + + static void setNpcValue(Index index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto& stats = ptr.getClass().getNpcStats(ptr); + if (prop == "progress") + stats.setLevelProgress(LuaUtil::cast(value)); + else if (prop == "skillIncreasesForAttribute") + stats.setSkillIncreasesForAttribute( + *std::get(index).getIf(), LuaUtil::cast(value)); + else if (prop == "skillIncreasesForSpecialization") + stats.setSkillIncreasesForSpecialization(std::get(index), LuaUtil::cast(value)); + } + + class SkillIncreasesForAttributeStats + { + ObjectVariant mObject; + + public: + SkillIncreasesForAttributeStats(ObjectVariant object) + : mObject(std::move(object)) + { + } + + sol::object get(const Context& context, ESM::StringRefId attributeId) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return sol::nil; + + return getValue(context, mObject, &setNpcValue, attributeId, "skillIncreasesForAttribute", + [attributeId](const MWWorld::Ptr& ptr) { + return ptr.getClass().getNpcStats(ptr).getSkillIncreasesForAttribute(attributeId); + }); + } + + void set(const Context& context, ESM::StringRefId attributeId, const sol::object& value) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return; + + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, attributeId, "skillIncreasesForAttribute" }] = value; + } + }; + + class SkillIncreasesForSpecializationStats + { + ObjectVariant mObject; + + public: + SkillIncreasesForSpecializationStats(ObjectVariant object) + : mObject(std::move(object)) + { + } + + sol::object get(const Context& context, int specialization) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return sol::nil; + + return getValue(context, mObject, &setNpcValue, specialization, "skillIncreasesForSpecialization", + [specialization](const MWWorld::Ptr& ptr) { + return ptr.getClass().getNpcStats(ptr).getSkillIncreasesForSpecialization(specialization); + }); + } + + void set(const Context& context, int specialization, const sol::object& value) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return; + + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, specialization, "skillIncreasesForSpecialization" }] + = value; + } + }; + class LevelStat { ObjectVariant mObject; @@ -85,7 +173,7 @@ namespace MWLua public: sol::object getCurrent(const Context& context) const { - return getValue(context, mObject, &LevelStat::setValue, std::monostate{}, "current", + return getValue(context, mObject, &setCreatureValue, std::monostate{}, "current", [](const MWWorld::Ptr& ptr) { return ptr.getClass().getCreatureStats(ptr).getLevel(); }); } @@ -93,7 +181,7 @@ namespace MWLua { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &LevelStat::setValue, std::monostate{}, "current" }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &setCreatureValue, std::monostate{}, "current" }] = value; } sol::object getProgress(const Context& context) const @@ -101,7 +189,30 @@ namespace MWLua const auto& ptr = mObject.ptr(); if (!ptr.getClass().isNpc()) return sol::nil; - return sol::make_object(context.mLua->sol(), ptr.getClass().getNpcStats(ptr).getLevelProgress()); + + return getValue(context, mObject, &setNpcValue, std::monostate{}, "progress", + [](const MWWorld::Ptr& ptr) { return ptr.getClass().getNpcStats(ptr).getLevelProgress(); }); + } + + void setProgress(const Context& context, const sol::object& value) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return; + + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, std::monostate{}, "progress" }] = value; + } + + SkillIncreasesForAttributeStats getSkillIncreasesForAttributeStats() const + { + return SkillIncreasesForAttributeStats{ mObject }; + } + + SkillIncreasesForSpecializationStats getSkillIncreasesForSpecializationStats() const + { + return SkillIncreasesForSpecializationStats{ mObject }; } static std::optional create(ObjectVariant object, Index) @@ -110,13 +221,6 @@ namespace MWLua return {}; return LevelStat{ std::move(object) }; } - - static void setValue(Index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) - { - auto& stats = ptr.getClass().getCreatureStats(ptr); - if (prop == "current") - stats.setLevel(LuaUtil::cast(value)); - } }; class DynamicStat @@ -323,6 +427,14 @@ namespace MWLua namespace sol { + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; template <> struct is_automagical : std::false_type { @@ -360,10 +472,39 @@ namespace MWLua sol::table stats(context.mLua->sol(), sol::create); actor["stats"] = LuaUtil::makeReadOnly(stats); + auto skillIncreasesForAttributeStatsT + = context.mLua->sol().new_usertype("SkillIncreasesForAttributeStats"); + for (auto attribute : MWBase::Environment::get().getESMStore()->get()) + { + skillIncreasesForAttributeStatsT[ESM::RefId(attribute.mId).serializeText()] = sol::property( + [=](const SkillIncreasesForAttributeStats& stat) { return stat.get(context, attribute.mId); }, + [=](const SkillIncreasesForAttributeStats& stat, const sol::object& value) { + stat.set(context, attribute.mId, value); + }); + } + // ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization) + auto skillIncreasesForSpecializationStatsT + = context.mLua->sol().new_usertype( + "skillIncreasesForSpecializationStats"); + for (int i = 0; i < 3; i++) + { + std::string_view index = ESM::Class::specializationIndexToLuaId.at(i); + skillIncreasesForSpecializationStatsT[index] + = sol::property([=](const SkillIncreasesForSpecializationStats& stat) { return stat.get(context, i); }, + [=](const SkillIncreasesForSpecializationStats& stat, const sol::object& value) { + stat.set(context, i, value); + }); + } + auto levelStatT = context.mLua->sol().new_usertype("LevelStat"); levelStatT["current"] = sol::property([context](const LevelStat& stat) { return stat.getCurrent(context); }, [context](const LevelStat& stat, const sol::object& value) { stat.setCurrent(context, value); }); - levelStatT["progress"] = sol::property([context](const LevelStat& stat) { return stat.getProgress(context); }); + levelStatT["progress"] = sol::property([context](const LevelStat& stat) { return stat.getProgress(context); }, + [context](const LevelStat& stat, const sol::object& value) { stat.setProgress(context, value); }); + levelStatT["skillIncreasesForAttribute"] + = sol::readonly_property([](const LevelStat& stat) { return stat.getSkillIncreasesForAttributeStats(); }); + levelStatT["skillIncreasesForSpecialization"] = sol::readonly_property( + [](const LevelStat& stat) { return stat.getSkillIncreasesForSpecializationStats(); }); stats["level"] = addIndexedAccessor(0); auto dynamicStatT = context.mLua->sol().new_usertype("DynamicStat"); @@ -461,6 +602,14 @@ namespace MWLua skillT["attribute"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string { return ESM::Attribute::indexToRefId(rec.mData.mAttribute).serializeText(); }); + skillT["skillGain1"] + = sol::readonly_property([](const ESM::Skill& rec) -> float { return rec.mData.mUseValue[0]; }); + skillT["skillGain2"] + = sol::readonly_property([](const ESM::Skill& rec) -> float { return rec.mData.mUseValue[1]; }); + skillT["skillGain3"] + = sol::readonly_property([](const ESM::Skill& rec) -> float { return rec.mData.mUseValue[2]; }); + skillT["skillGain4"] + = sol::readonly_property([](const ESM::Skill& rec) -> float { return rec.mData.mUseValue[3]; }); auto schoolT = context.mLua->sol().new_usertype("MagicSchool"); schoolT[sol::meta_function::to_string] diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index a4e7168b0f..1f8265aab5 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1962,7 +1962,7 @@ namespace MWMechanics mSneakSkillTimer = 0.f; if (avoidedNotice && mSneakSkillTimer == 0.f) - player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 0); + player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, ESM::Skill::Sneak_AvoidNotice); if (!detected) MWBase::Environment::get().getWindowManager()->setSneakVisibility(true); diff --git a/apps/openmw/mwmechanics/alchemy.cpp b/apps/openmw/mwmechanics/alchemy.cpp index aea3e36632..5a995e7f06 100644 --- a/apps/openmw/mwmechanics/alchemy.cpp +++ b/apps/openmw/mwmechanics/alchemy.cpp @@ -335,7 +335,7 @@ void MWMechanics::Alchemy::addPotion(const std::string& name) void MWMechanics::Alchemy::increaseSkill() { - mAlchemist.getClass().skillUsageSucceeded(mAlchemist, ESM::Skill::Alchemy, 0); + mAlchemist.getClass().skillUsageSucceeded(mAlchemist, ESM::Skill::Alchemy, ESM::Skill::Alchemy_CreatePotion); } float MWMechanics::Alchemy::getAlchemyFactor() const diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 8d69da2c43..7330b72255 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2088,7 +2088,7 @@ namespace MWMechanics mSecondsOfSwimming += duration; while (mSecondsOfSwimming > 1) { - cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 1); + cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, ESM::Skill::Athletics_SwimOneSecond); mSecondsOfSwimming -= 1; } } @@ -2097,7 +2097,7 @@ namespace MWMechanics mSecondsOfRunning += duration; while (mSecondsOfRunning > 1) { - cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 0); + cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, ESM::Skill::Athletics_RunOneSecond); mSecondsOfRunning -= 1; } } @@ -2215,7 +2215,7 @@ namespace MWMechanics { // report acrobatics progression if (isPlayer) - cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 1); + cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, ESM::Skill::Acrobatics_Fall); } } diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 3208ea2293..06a1c3b9cc 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -167,7 +167,7 @@ namespace MWMechanics blockerStats.setBlock(true); if (blocker == getPlayer()) - blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0); + blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, ESM::Skill::Block_Success); return true; } @@ -267,7 +267,7 @@ namespace MWMechanics applyWerewolfDamageMult(victim, projectile, damage); if (attacker == getPlayer()) - attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, 0); + attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, ESM::Skill::Weapon_SuccessfulHit); const MWMechanics::AiSequence& sequence = victim.getClass().getCreatureStats(victim).getAiSequence(); bool unaware = attacker == getPlayer() && !sequence.isInCombat() diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 13894146b3..1de55b0c8d 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -84,7 +84,8 @@ namespace MWMechanics if (getEnchantChance() <= (Misc::Rng::roll0to99(prng))) return false; - mEnchanter.getClass().skillUsageSucceeded(mEnchanter, ESM::Skill::Enchant, 2); + mEnchanter.getClass().skillUsageSucceeded( + mEnchanter, ESM::Skill::Enchant, ESM::Skill::Enchant_CreateMagicItem); } enchantment.mEffects = mEffectList; diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index 808059fccd..046c94fc8f 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -298,6 +298,11 @@ int MWMechanics::NpcStats::getLevelProgress() const return mLevelProgress; } +void MWMechanics::NpcStats::setLevelProgress(int progress) +{ + mLevelProgress = progress; +} + void MWMechanics::NpcStats::levelUp() { const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); @@ -344,11 +349,33 @@ int MWMechanics::NpcStats::getLevelupAttributeMultiplier(ESM::Attribute::Attribu return MWBase::Environment::get().getESMStore()->get().find(gmst.str())->mValue.getInteger(); } +int MWMechanics::NpcStats::getSkillIncreasesForAttribute(ESM::Attribute::AttributeID attribute) const +{ + auto it = mSkillIncreases.find(attribute); + if (it == mSkillIncreases.end()) + return 0; + return it->second; +} + +void MWMechanics::NpcStats::setSkillIncreasesForAttribute(ESM::Attribute::AttributeID attribute, int increases) +{ + if (increases == 0) + mSkillIncreases.erase(attribute); + else + mSkillIncreases[attribute] = increases; +} + int MWMechanics::NpcStats::getSkillIncreasesForSpecialization(int spec) const { return mSpecIncreases[spec]; } +void MWMechanics::NpcStats::setSkillIncreasesForSpecialization(int spec, int increases) +{ + assert(spec >= 0 && spec < 3); + mSpecIncreases[spec] = increases; +} + void MWMechanics::NpcStats::flagAsUsed(const ESM::RefId& id) { mUsedIds.insert(id); diff --git a/apps/openmw/mwmechanics/npcstats.hpp b/apps/openmw/mwmechanics/npcstats.hpp index 7113ee6207..9695d45ac4 100644 --- a/apps/openmw/mwmechanics/npcstats.hpp +++ b/apps/openmw/mwmechanics/npcstats.hpp @@ -92,10 +92,14 @@ namespace MWMechanics void increaseSkill(ESM::RefId id, const ESM::Class& class_, bool preserveProgress, bool readBook = false); int getLevelProgress() const; + void setLevelProgress(int progress); int getLevelupAttributeMultiplier(ESM::Attribute::AttributeID attribute) const; + int getSkillIncreasesForAttribute(ESM::Attribute::AttributeID attribute) const; + void setSkillIncreasesForAttribute(ESM::Attribute::AttributeID, int increases); int getSkillIncreasesForSpecialization(int spec) const; + void setSkillIncreasesForSpecialization(int spec, int increases); void levelUp(); diff --git a/apps/openmw/mwmechanics/recharge.cpp b/apps/openmw/mwmechanics/recharge.cpp index 6e16436bcc..7b0ad75d3c 100644 --- a/apps/openmw/mwmechanics/recharge.cpp +++ b/apps/openmw/mwmechanics/recharge.cpp @@ -84,7 +84,7 @@ namespace MWMechanics MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Enchant Fail")); } - player.getClass().skillUsageSucceeded(player, ESM::Skill::Enchant, 0); + player.getClass().skillUsageSucceeded(player, ESM::Skill::Enchant, ESM::Skill::Enchant_Recharge); gem.getContainerStore()->remove(gem, 1); if (gem.getCellRef().getCount() == 0) diff --git a/apps/openmw/mwmechanics/repair.cpp b/apps/openmw/mwmechanics/repair.cpp index 3011004244..914fa0b542 100644 --- a/apps/openmw/mwmechanics/repair.cpp +++ b/apps/openmw/mwmechanics/repair.cpp @@ -70,7 +70,7 @@ namespace MWMechanics stacked->getRefData().getLocals().setVarByInt(script, "onpcrepair", 1); // increase skill - player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, 0); + player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, ESM::Skill::Armorer_Repair); MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Repair")); MWBase::Environment::get().getWindowManager()->messageBox("#{sRepairSuccess}"); diff --git a/apps/openmw/mwmechanics/security.cpp b/apps/openmw/mwmechanics/security.cpp index a13131cae6..0fb8a95699 100644 --- a/apps/openmw/mwmechanics/security.cpp +++ b/apps/openmw/mwmechanics/security.cpp @@ -64,7 +64,7 @@ namespace MWMechanics lock.getCellRef().unlock(); resultMessage = "#{sLockSuccess}"; resultSound = "Open Lock"; - mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 1); + mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, ESM::Skill::Security_PickLock); } else resultMessage = "#{sLockFail}"; @@ -115,7 +115,7 @@ namespace MWMechanics resultSound = "Disarm Trap"; resultMessage = "#{sTrapSuccess}"; - mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 0); + mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, ESM::Skill::Security_DisarmTrap); } else resultMessage = "#{sTrapFail}"; diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 0496033c70..721131dcb0 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -354,7 +354,7 @@ namespace MWMechanics if (type == ESM::Enchantment::WhenUsed) { if (mCaster == getPlayer()) - mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, 1); + mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, ESM::Skill::Enchant_UseMagicItem); } else if (type == ESM::Enchantment::CastOnce) { @@ -364,7 +364,7 @@ namespace MWMechanics else if (type == ESM::Enchantment::WhenStrikes) { if (mCaster == getPlayer()) - mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, 3); + mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, ESM::Skill::Enchant_CastOnStrike); } if (isProjectile) @@ -439,7 +439,7 @@ namespace MWMechanics } if (!mManualSpell && mCaster == getPlayer() && spellIncreasesSkill(spell)) - mCaster.getClass().skillUsageSucceeded(mCaster, school, 0); + mCaster.getClass().skillUsageSucceeded(mCaster, school, ESM::Skill::Spellcast_Success); // 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/apps/openmw/mwmechanics/trading.cpp b/apps/openmw/mwmechanics/trading.cpp index 9500897f25..b7e361c0b9 100644 --- a/apps/openmw/mwmechanics/trading.cpp +++ b/apps/openmw/mwmechanics/trading.cpp @@ -79,7 +79,8 @@ namespace MWMechanics { skillGain = std::floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer); } - player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0, skillGain); + player.getClass().skillUsageSucceeded( + player, ESM::Skill::Mercantile, ESM::Skill::Mercantile_Success, skillGain); return true; } diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 149113dfb1..bd2d43dd5a 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -69,7 +69,7 @@ namespace // Advance acrobatics and set flag for GetPCJumping if (isPlayer) { - ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, 0); + ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, ESM::Skill::Acrobatics_Jump); MWBase::Environment::get().getWorld()->getPlayer().setJumping(true); } diff --git a/apps/openmw/mwworld/actioneat.cpp b/apps/openmw/mwworld/actioneat.cpp index b77fe146ef..c67502bdee 100644 --- a/apps/openmw/mwworld/actioneat.cpp +++ b/apps/openmw/mwworld/actioneat.cpp @@ -11,7 +11,7 @@ namespace MWWorld void ActionEat::executeImp(const Ptr& actor) { if (actor.getClass().consume(getTarget(), actor) && actor == MWMechanics::getPlayer()) - actor.getClass().skillUsageSucceeded(actor, ESM::Skill::Alchemy, 1); + actor.getClass().skillUsageSucceeded(actor, ESM::Skill::Alchemy, ESM::Skill::Alchemy_UseIngredient); } ActionEat::ActionEat(const MWWorld::Ptr& object) diff --git a/components/esm3/loadskil.hpp b/components/esm3/loadskil.hpp index 978e3d5dc4..9cae87903c 100644 --- a/components/esm3/loadskil.hpp +++ b/components/esm3/loadskil.hpp @@ -47,13 +47,45 @@ namespace ESM uint32_t mRecordFlags; SkillId mId; + //! Enum that defines the index into SKDTstruct::mUseValue for all vanilla skill uses + enum UseType + { + // These are shared by multiple skills + Armor_HitByOpponent = 0, + Block_Success = 0, + Spellcast_Success = 0, + Weapon_SuccessfulHit = 0, + + // Skill-specific use types + Alchemy_CreatePotion = 0, + Alchemy_UseIngredient = 1, + Enchant_Recharge = 0, + Enchant_UseMagicItem = 1, + Enchant_CreateMagicItem = 2, + Enchant_CastOnStrike = 3, + Acrobatics_Jump = 0, + Acrobatics_Fall = 1, + Mercantile_Success = 0, + Mercantile_Bribe = 1, //!< \Note This is bugged in vanilla and is not actually in use. + Security_DisarmTrap = 0, + Security_PickLock = 1, + Sneak_AvoidNotice = 0, + Sneak_PickPocket = 1, + Speechcraft_Success = 0, + Speechcraft_Fail = 1, + Armorer_Repair = 0, + Athletics_RunOneSecond = 0, + Athletics_SwimOneSecond = 0, + + }; + struct SKDTstruct { int32_t mAttribute; // see defs.hpp int32_t mSpecialization; // 0 - Combat, 1 - Magic, 2 - Stealth float mUseValue[4]; // How much skill improves through use. Meaning // of each field depends on what skill this - // is. We should document this better later. + // is. See UseType above }; // Total size: 24 bytes SKDTstruct mData; diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 02b03cbd69..a9f7c71067 100755 --- a/docs/source/luadoc_data_paths.sh +++ b/docs/source/luadoc_data_paths.sh @@ -9,5 +9,6 @@ paths=( scripts/omw/settings/player.lua scripts/omw/ui.lua scripts/omw/usehandlers.lua + scripts/omw/skillhandlers.lua ) printf '%s\n' "${paths[@]}" \ No newline at end of file diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index 1bb7e0b6e9..2f4708810b 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -40,6 +40,7 @@ Lua API reference interface_item_usage interface_mwui interface_settings + interface_skill_progression interface_ui iterables diff --git a/docs/source/reference/lua-scripting/interface_skill_progression.rst b/docs/source/reference/lua-scripting/interface_skill_progression.rst new file mode 100644 index 0000000000..5cd2e87812 --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_skill_progression.rst @@ -0,0 +1,6 @@ +Interface SkillProgression +=================== + +.. raw:: html + :file: generated_html/scripts_omw_skillhandlers.html + diff --git a/docs/source/reference/lua-scripting/tables/interfaces.rst b/docs/source/reference/lua-scripting/tables/interfaces.rst index 5029baf0a3..0692e60b1c 100644 --- a/docs/source/reference/lua-scripting/tables/interfaces.rst +++ b/docs/source/reference/lua-scripting/tables/interfaces.rst @@ -25,6 +25,10 @@ - by global scripts - | Allows to extend or override built-in item usage | mechanics. + * - :ref:`SkillProgression ` + - by local scripts + - | Control, extend, and override skill progression of the + - | player. * - :ref:`Settings ` - by player and global scripts - Save, display and track changes of setting values. diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 3ab30c87ff..a0c97ad308 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -90,6 +90,7 @@ set(BUILTIN_DATA_FILES scripts/omw/mwui/textEdit.lua scripts/omw/mwui/space.lua scripts/omw/mwui/init.lua + scripts/omw/skillhandlers.lua scripts/omw/ui.lua scripts/omw/usehandlers.lua scripts/omw/worldeventhandlers.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index a6f4ca5f33..a07b62c5fb 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -16,6 +16,7 @@ PLAYER: scripts/omw/playercontrols.lua PLAYER: scripts/omw/camera/camera.lua PLAYER: scripts/omw/input/actionbindings.lua PLAYER: scripts/omw/input/smoothmovement.lua +PLAYER: scripts/omw/skillhandlers.lua NPC,CREATURE: scripts/omw/ai.lua # User interface diff --git a/files/data/scripts/omw/mechanics/playercontroller.lua b/files/data/scripts/omw/mechanics/playercontroller.lua index 19e62d02c7..29baf32020 100644 --- a/files/data/scripts/omw/mechanics/playercontroller.lua +++ b/files/data/scripts/omw/mechanics/playercontroller.lua @@ -1,7 +1,13 @@ +local ambient = require('openmw.ambient') local core = require('openmw.core') +local Skill = core.stats.Skill +local I = require('openmw.interfaces') local nearby = require('openmw.nearby') local self = require('openmw.self') local types = require('openmw.types') +local NPC = types.NPC +local Actor = types.Actor +local ui = require('openmw.ui') local cell = nil local autodoors = {} @@ -31,6 +37,69 @@ local function processAutomaticDoors() end end +local function skillLevelUpHandler(skillid, source, params) + local skillStat = NPC.stats.skills[skillid](self) + if skillStat.base >= 100 then + return false + end + + if params.skillIncreaseValue then + skillStat.base = skillStat.base + params.skillIncreaseValue + end + + local levelStat = Actor.stats.level(self) + if params.levelUpProgress then + levelStat.progress = levelStat.progress + params.levelUpProgress + end + + if params.levelUpAttribute and params.levelUpAttributeIncreaseValue then + levelStat.skillIncreasesForAttribute[params.levelUpAttribute] + = levelStat.skillIncreasesForAttribute[params.levelUpAttribute] + params.levelUpAttributeIncreaseValue + end + + if params.levelUpSpecialization and params.levelUpSpecializationIncreaseValue then + levelStat.skillIncreasesForSpecialization[params.levelUpSpecialization] + = levelStat.skillIncreasesForSpecialization[params.levelUpSpecialization] + params.levelUpSpecializationIncreaseValue; + end + + local skillRecord = Skill.record(skillid) + local npcRecord = NPC.record(self) + local class = NPC.classes.record(npcRecord.class) + + ambient.playSound("skillraise") + + local message = core.getGMST('sNotifyMessage39') + message = message:gsub("%%s", skillRecord.name) + message = message:gsub("%%d", tostring(skillStat.base)) + + if source == I.SkillProgression.SKILL_INCREASE_SOURCES.Book then + message = '#{sBookSkillMessage}\n'..message + end + + ui.showMessage(message) + + if levelStat.progress >= core.getGMST('iLevelUpTotal') then + ui.showMessage('#{sLevelUpMsg}') + end + + if not source or source == I.SkillProgression.SKILL_INCREASE_SOURCES.Usage then skillStat.progress = 0 end +end + +local function skillUsedHandler(skillid, useType, params) + if NPC.isWerewolf(self) then + return false + end + + if params.skillGain then + local skillStat = NPC.stats.skills[skillid](self) + skillStat.progress = skillStat.progress + params.skillGain + + if skillStat.progress >= 1 then + I.SkillProgression.skillLevelUp(skillid, I.SkillProgression.SKILL_INCREASE_SOURCES.Usage) + end + end +end + local function onUpdate() if self.cell ~= cell then cell = self.cell @@ -39,8 +108,14 @@ local function onUpdate() processAutomaticDoors() end +local function onActive() + I.SkillProgression.addSkillUsedHandler(skillUsedHandler) + I.SkillProgression.addSkillLevelUpHandler(skillLevelUpHandler) +end + return { engineHandlers = { onUpdate = onUpdate, + onActive = onActive, }, } diff --git a/files/data/scripts/omw/skillhandlers.lua b/files/data/scripts/omw/skillhandlers.lua new file mode 100644 index 0000000000..a4fb42ebde --- /dev/null +++ b/files/data/scripts/omw/skillhandlers.lua @@ -0,0 +1,283 @@ +local self = require('openmw.self') +local types = require('openmw.types') +local core = require('openmw.core') +local NPC = require('openmw.types').NPC +local Skill = core.stats.Skill + +--- +-- Table of skill use types defined by morrowind. +-- Each entry corresponds to an index into the available skill gain values +-- of a @{openmw.types#SkillRecord} +-- @type SkillUseType +-- @field #number Armor_HitByOpponent 0 +-- @field #number Block_Success 0 +-- @field #number Spellcast_Success 0 +-- @field #number Weapon_SuccessfulHit 0 +-- @field #number Alchemy_CreatePotion 0 +-- @field #number Alchemy_UseIngredient 1 +-- @field #number Enchant_Recharge 0 +-- @field #number Enchant_UseMagicItem 1 +-- @field #number Enchant_CreateMagicItem 2 +-- @field #number Enchant_CastOnStrike 3 +-- @field #number Acrobatics_Jump 0 +-- @field #number Acrobatics_Fall 1 +-- @field #number Mercantile_Success 0 +-- @field #number Mercantile_Bribe 1 +-- @field #number Security_DisarmTrap 0 +-- @field #number Security_PickLock 1 +-- @field #number Sneak_AvoidNotice 0 +-- @field #number Sneak_PickPocket 1 +-- @field #number Speechcraft_Success 0 +-- @field #number Speechcraft_Fail 1 +-- @field #number Armorer_Repair 0 +-- @field #number Athletics_RunOneSecond 0 +-- @field #number Athletics_SwimOneSecond 0 + +--- +-- Table of valid sources for skill increases +-- @type SkillLevelUpSource +-- @field #string Book book +-- @field #string Trainer trainer +-- @field #string Usage usage + +--- +-- Table of valid handler signatures +-- @type HandlerSignatures +-- + +--- Signature of the skillLevelUp handler +-- @function [parent=#HandlerSignatures] skillLevelUpHandler +-- @param #string skillid The ID of the skill being leveled up +-- @param #SkillLevelUpSource source The source of the skill level up +-- @param #table params Modifiable skill level up values as a table. These values are calculated based on vanilla mechanics. Setting any value to nil will cause that mechanic to be skipped. By default contains these values: +-- +-- * `skillIncreaseValue` - The numeric amount of skill levels gained. +-- * `levelUpProgress` - The numeric amount of level up progress gained. +-- * `levelUpAttribute` - The string identifying the attribute that should receive points from this skill level up. +-- * `levelUpAttributeIncreaseValue` - The numeric amount of attribute increase points received. This contributes to the amount of each attribute the character receives during a vanilla level up. +-- * `levelUpSpecialization` - The string identifying the specialization that should receive points from this skill level up. +-- * `levelUpSpecializationIncreaseValue` - The numeric amount of specialization increase points received. This contributes to the icon displayed at the level up screen during a vanilla level up. + +--- Signature of the skillUsed handler +-- @function [parent=#HandlerSignatures] skillUsedHandler +-- @param #string skillid The ID of the skill being progressed +-- @param #SkillUseType useType The use type the skill progression +-- @param #table params Modifiable skill progression value. By default contains the single value +-- +-- * `skillGain` - The numeric amount of skill progress gained, normalized to the range 0 to 1, where 1 is a full level. + + +local skillUsedHandlers = {} +local skillLevelUpHandlers = {} + +local function tableHasValue(table, value) + for _, v in pairs(table) do + if v == value then return true end + end + return false +end + +local function getSkillGainValue(skillid, useType, scale) + local skillRecord = Skill.record(skillid) + + local skillGain = 0 + if useType == 0 then + skillGain = skillRecord.skillGain1 + elseif useType == 1 then + skillGain = skillRecord.skillGain2 + elseif useType == 2 then + skillGain = skillRecord.skillGain3 + elseif useType == 3 then + skillGain = skillRecord.skillGain4 + end + + if scale ~= nil then skillGain = skillGain * scale end + return skillGain +end + +local function getSkillProgressRequirementUnorm(npc, skillid) + local npcRecord = NPC.record(npc) + local class = NPC.classes.record(npcRecord.class) + local skillStat = NPC.stats.skills[skillid](npc) + local skillRecord = Skill.record(skillid) + + local factor = core.getGMST('fMiscSkillBonus') + if tableHasValue(class.majorSkills, skillid) then + factor = core.getGMST('fMajorSkillBonus') + elseif tableHasValue(class.minorSkills, skillid) then + factor = core.getGMST('fMinorSkillBonus') + end + + if skillRecord.specialization == class.specialization then + factor = factor * core.getGMST('fSpecialSkillBonus') + end + + return (skillStat.base + 1) * factor +end + +local function skillUsed(skillid, useType, scale) + if #skillUsedHandlers == 0 then + -- If there are no handlers, then there won't be any effect, so skip calculations + return + end + + if useType > 3 or useType < 0 then + print('Error: Unknown useType: '..tostring(useType)) + return + end + + -- Compute skill gain + local skillStat = NPC.stats.skills[skillid](self) + local skillGainUnorm = getSkillGainValue(skillid, useType, scale) + local skillProgressRequirementUnorm = getSkillProgressRequirementUnorm(self, skillid) + local skillGain = skillGainUnorm / skillProgressRequirementUnorm + + -- Put skill gain in a table so that handlers can modify it + local parameters = { + skillGain = skillGain, + } + + for i = #skillUsedHandlers, 1, -1 do + if skillUsedHandlers[i](skillid, useType, parameters) == false then + return + end + end +end + +local function skillLevelUp(skillid, source) + if #skillLevelUpHandlers == 0 then + -- If there are no handlers, then there won't be any effect, so skip calculations + return + end + + local skillRecord = Skill.record(skillid) + local npcRecord = NPC.record(self) + local class = NPC.classes.record(npcRecord.class) + + local levelUpProgress = 0 + local levelUpAttributeIncreaseValue = core.getGMST('iLevelupMiscMultAttriubte') + + if tableHasValue(class.minorSkills, skillid) then + levelUpProgress = core.getGMST('iLevelUpMinorMult') + levelUpAttributeIncreaseValue = core.getGMST('iLevelUpMinorMultAttribute') + elseif tableHasValue(class.majorSkills, skillid) then + levelUpProgress = core.getGMST('iLevelUpMajorMult') + levelUpAttributeIncreaseValue = core.getGMST('iLevelUpMajorMultAttribute') + end + + local parameters = + { + skillIncreaseValue = 1, + levelUpProgress = levelUpProgress, + levelUpAttribute = skillRecord.attribute, + levelUpAttributeIncreaseValue = levelUpAttributeIncreaseValue, + levelUpSpecialization = skillRecord.specialization, + levelUpSpecializationIncreaseValue = core.getGMST('iLevelupSpecialization'), + } + + for i = #skillLevelUpHandlers, 1, -1 do + if skillLevelUpHandlers[i](skillid, source, parameters) == false then + return + end + end +end + +return { + interfaceName = 'SkillProgression', + --- + -- Allows to extend or override built-in skill progression mechanics. + -- @module SkillProgression + -- @usage local I = require('openmw.interfaces') + -- + -- -- Forbid increasing destruction skill past 50 + -- I.SkillProgression.addSkillLevelUpHandler(function(skillid, options) + -- if skillid == 'destruction' and types.NPC.stats.skills.destruction(self).base >= 50 then + -- return false + -- end + -- end) + -- + -- -- Scale sneak skill progression based on active invisibility effects + -- I.SkillProgression.addSkillUsedHandler(function(skillid, useType, params) + -- if skillid == 'sneak' and useType == I.SkillProgression.SKILL_USE_TYPES.Sneak_AvoidNotice then + -- local activeEffects = Actor.activeEffects(self) + -- local visibility = activeEffects:getEffect(core.magic.EFFECT_TYPE.Chameleon).magnitude / 100 + -- visibility = visibility + activeEffects:getEffect(core.magic.EFFECT_TYPE.Invisibility).magnitude + -- visibility = 1 - math.min(1, math.max(0, visibility)) + -- local oldSkillGain = params.skillGain + -- params.skillGain = oldSkillGain * visibility + -- end + -- end + -- + interface = { + --- Interface version + -- @field [parent=#SkillProgression] #number version + version = 0, + + --- Add new skill level up handler for this actor + -- @function [parent=#SkillProgression] addSkillLevelUpHandler + -- @param #function handler The handler, see #skillLevelUpHandler. + addSkillLevelUpHandler = function(handler) + skillLevelUpHandlers[#skillLevelUpHandlers + 1] = handler + end, + + --- Add new skillUsed handler for this actor + -- @function [parent=#SkillProgression] addSkillUsedHandler + -- @param #function handler The handler. + addSkillUsedHandler = function(handler) + skillUsedHandlers[#skillUsedHandlers + 1] = handler + end, + + --- Register a skill use, activating relevant handlers + -- @function [parent=#SkillProgression] skillUsed + -- @param #string skillid The if of the skill that was used + -- @param #SkillUseType useType A number from 0 to 3 (inclusive) representing the way the skill was used, with each use type having a different skill progression rate. Available use types and its effect is skill specific. See @{SkillProgression#skillUseType} + -- @param #number scale A number that linearly scales the skill progress received from this use. Defaults to 1. + skillUsed = skillUsed, + + --- @{#SkillUseType} + -- @field [parent=#SkillProgression] #SkillUseType SKILL_USE_TYPES Available skill usage types + SKILL_USE_TYPES = { + -- These are shared by multiple skills + Armor_HitByOpponent = 0, + Block_Success = 0, + Spellcast_Success = 0, + Weapon_SuccessfulHit = 0, + + -- Skill-specific use types + Alchemy_CreatePotion = 0, + Alchemy_UseIngredient = 1, + Enchant_Recharge = 0, + Enchant_UseMagicItem = 1, + Enchant_CreateMagicItem = 2, + Enchant_CastOnStrike = 3, + Acrobatics_Jump = 0, + Acrobatics_Fall = 1, + Mercantile_Success = 0, + Mercantile_Bribe = 1, -- Note: This is bugged in vanilla and is not actually in use. + Security_DisarmTrap = 0, + Security_PickLock = 1, + Sneak_AvoidNotice = 0, + Sneak_PickPocket = 1, + Speechcraft_Success = 0, + Speechcraft_Fail = 1, + Armorer_Repair = 0, + Athletics_RunOneSecond = 0, + Athletics_SwimOneSecond = 0, + }, + + --- Register a skill increase, activating relevant handlers + -- @function [parent=#SkillProgression] skillUsed + -- @param #string skillid The id of the skill to be increased. + -- @param #SkillLevelUpSource source The source of the skill increase. + skillLevelUp = skillLevelUp, + + --- @{#SkillLevelUpSource} + -- @field [parent=#SkillProgression] #SkillLevelUpSource SKILL_INCREASE_SOURCES + SKILL_INCREASE_SOURCES = { + Book = 'book', + Usage = 'usage', + Trainer = 'trainer', + }, + }, + engineHandlers = { _onSkillUse = skillUsed }, +} diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 94d63312f1..6babfa6c85 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -875,6 +875,10 @@ -- @field #string specialization Skill specialization. Either combat, magic, or stealth. -- @field #MagicSchoolData school Optional magic school -- @field #string attribute The id of the skill's governing attribute +-- @field #string skillGain1 1 of 4 possible skill gain values. See [SkillProgression#SkillUseType](interface_skill_progression.html#SkillUseType) +-- @field #string skillGain2 2 of 4 possible skill gain values. See [SkillProgression#SkillUseType](interface_skill_progression.html#SkillUseType) +-- @field #string skillGain3 3 of 4 possible skill gain values. See [SkillProgression#SkillUseType](interface_skill_progression.html#SkillUseType) +-- @field #string skillGain4 4 of 4 possible skill gain values. See [SkillProgression#SkillUseType](interface_skill_progression.html#SkillUseType) --- -- @type MagicSchoolData diff --git a/files/lua_api/openmw/interfaces.lua b/files/lua_api/openmw/interfaces.lua index 57103768d2..5ed98bd8be 100644 --- a/files/lua_api/openmw/interfaces.lua +++ b/files/lua_api/openmw/interfaces.lua @@ -26,6 +26,9 @@ --- -- @field [parent=#interfaces] scripts.omw.usehandlers#scripts.omw.usehandlers ItemUsage +--- +-- @field [parent=#interfaces] scripts.omw.skillhandlers#scripts.omw.skillhandlers SkillProgression + --- -- @function [parent=#interfaces] __index -- @param #interfaces self diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 734288bfb7..c74b599597 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -352,10 +352,29 @@ -- @function [parent=#ActorSpells] clear -- @param self +--- Values affect how much each attribute can be increased at level up, and are all reset to 0 upon level up. +-- @type SkillIncreasesForAttributeStats +-- @field #number agility Number of contributions to agility for the next level up. +-- @field #number endurance Number of contributions to endurance for the next level up. +-- @field #number intelligence Number of contributions to intelligence for the next level up. +-- @field #number luck Number of contributions to luck for the next level up. +-- @field #number personality Number of contributions to personality for the next level up. +-- @field #number speed Number of contributions to speed for the next level up. +-- @field #number strength Number of contributions to strength for the next level up. +-- @field #number willpower Number of contributions to willpower for the next level up. + +--- Values affect the graphic used on the level up screen, and are all reset to 0 upon level up. +-- @type SkillIncreasesForSpecializationStats +-- @field #number combat Number of contributions to combat specialization for the next level up. +-- @field #number magic Number of contributions to magic specialization for the next level up. +-- @field #number stealth Number of contributions to stealth specialization for the next level up. + --- -- @type LevelStat -- @field #number current The actor's current level. --- @field #number progress The NPC's level progress (read-only.) +-- @field #number progress The NPC's level progress. +-- @field #SkillIncreasesForAttributeStats skillIncreasesForAttribute The NPC's attribute contributions towards the next level up. Values affect how much each attribute can be increased at level up. +-- @field #SkillIncreasesForSpecializationStats skillIncreasesForSpecialization The NPC's attribute contributions towards the next level up. Values affect the graphic used on the level up screen. --- -- @type DynamicStat