diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b84b7296..8d8d6f8ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Bug #4382: Sound output device does not change when it should Bug #4508: Can't stack enchantment buffs from different instances of the same self-cast generic magic apparel Bug #4610: Casting a Bound Weapon spell cancels the casting animation by equipping the weapon prematurely + Bug #4683: Disposition decrease when player commits crime is not implemented properly Bug #4742: Actors with wander never stop walking after Loopgroup Walkforward Bug #4743: PlayGroup doesn't play non-looping animations correctly Bug #4754: Stack of ammunition cannot be equipped partially diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 79673463ef..0e44b8c03e 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -23,9 +23,11 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" #include "bookpage.hpp" #include "textcolours.hpp" @@ -736,6 +738,15 @@ namespace MWGui bool dispositionVisible = false; if (!mPtr.isEmpty() && mPtr.getClass().isNpc()) { + // If actor was a witness to a crime which was payed off, + // restore original disposition immediately. + MWMechanics::NpcStats& npcStats = mPtr.getClass().getNpcStats(mPtr); + if (npcStats.getCrimeId() != -1 && npcStats.getCrimeDispositionModifier() != 0) + { + if (npcStats.getCrimeId() <= MWBase::Environment::get().getWorld()->getPlayer().getCrimeId()) + npcStats.setCrimeDispositionModifier(0); + } + dispositionVisible = true; mDispositionBar->setProgressRange(100); mDispositionBar->setProgressPosition( diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index e0baac0764..afb7ec3e8e 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1164,6 +1164,9 @@ namespace MWMechanics creatureStats.setAlarmed(false); creatureStats.setAiSetting(AiSetting::Fight, ptr.getClass().getBaseFightRating(ptr)); + // Restore original disposition + npcStats.setCrimeDispositionModifier(0); + // Update witness crime id npcStats.setCrimeId(-1); } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 8a64af1cbd..3a253d9dc3 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -473,8 +473,8 @@ namespace MWMechanics int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp) { - const MWMechanics::NpcStats& npcSkill = ptr.getClass().getNpcStats(ptr); - float x = static_cast(npcSkill.getBaseDisposition()); + const MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + float x = static_cast(npcStats.getBaseDisposition() + npcStats.getCrimeDispositionModifier()); MWWorld::LiveCellRef* npc = ptr.get(); MWWorld::Ptr playerPtr = getPlayer(); @@ -1287,62 +1287,134 @@ namespace MWMechanics if (!canReportCrime(actor, victim, playerFollowers)) continue; - if (reported && actor.getClass().isClass(actor, "guard")) + NpcStats& observerStats = actor.getClass().getNpcStats(actor); + + int alarm = observerStats.getAiSetting(AiSetting::Alarm).getBase(); + float alarmTerm = 0.01f * alarm; + + bool isActorVictim = actor == victim; + float dispTerm = isActorVictim ? dispVictim : disp; + + bool isActorGuard = actor.getClass().isClass(actor, "guard"); + + int currentDisposition = getDerivedDisposition(actor); + + bool isPermanent = false; + bool applyOnlyIfHostile = false; + int dispositionModifier = 0; + // Murdering and trespassing seem to do not affect disposition + if (type == OT_Theft) + { + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else if (type == OT_Pickpocket) + { + if (alarm >= 100 && isActorGuard) + dispositionModifier = static_cast(dispTerm); + else if (isActorVictim && isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else if (isActorVictim) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm); + } + } + else if (type == OT_Assault) + { + if (isActorVictim && !isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm); + } + else if (alarm >= 100) + dispositionModifier = static_cast(dispTerm); + else if (isActorVictim && isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else + { + applyOnlyIfHostile = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + } + + bool setCrimeId = false; + if (isPermanent && dispositionModifier != 0 && !applyOnlyIfHostile) + { + setCrimeId = true; + dispositionModifier = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + int baseDisposition = observerStats.getBaseDisposition(); + observerStats.setBaseDisposition(baseDisposition + dispositionModifier); + } + else if (dispositionModifier != 0 && !applyOnlyIfHostile) + { + setCrimeId = true; + dispositionModifier = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + observerStats.modCrimeDispositionModifier(dispositionModifier); + } + + if (isActorGuard && alarm >= 100) { // Mark as Alarmed for dialogue - actor.getClass().getCreatureStats(actor).setAlarmed(true); + observerStats.setAlarmed(true); - // Set the crime ID, which we will use to calm down participants - // once the bounty has been paid. - actor.getClass().getNpcStats(actor).setCrimeId(id); + setCrimeId = true; - if (!actor.getClass().getCreatureStats(actor).getAiSequence().isInPursuit()) + if (!observerStats.getAiSequence().isInPursuit()) { - actor.getClass().getCreatureStats(actor).getAiSequence().stack(AiPursue(player), actor); + observerStats.getAiSequence().stack(AiPursue(player), actor); } } else { - float dispTerm = (actor == victim) ? dispVictim : disp; - - float alarmTerm - = 0.01f * actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Alarm).getBase(); - if (type == OT_Pickpocket && alarmTerm <= 0) + // If Alarm is 0, treat it like 100 to calculate a Fight modifier for a victim of pickpocketing. + // Observers which do not try to arrest player do not care about pickpocketing at all. + if (type == OT_Pickpocket && isActorVictim && alarmTerm == 0.0) alarmTerm = 1.0; + else if (type == OT_Pickpocket && !isActorVictim) + alarmTerm = 0.0; - if (actor != victim) - dispTerm *= alarmTerm; - - float fightTerm = static_cast((actor == victim) ? fightVictim : fight); + float fightTerm = static_cast(isActorVictim ? fightVictim : fight); fightTerm += getFightDispositionBias(dispTerm); fightTerm += getFightDistanceBias(actor, player); fightTerm *= alarmTerm; - const int observerFightRating - = actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Fight).getBase(); + const int observerFightRating = observerStats.getAiSetting(AiSetting::Fight).getBase(); if (observerFightRating + fightTerm > 100) fightTerm = static_cast(100 - observerFightRating); fightTerm = std::max(0.f, fightTerm); if (observerFightRating + fightTerm >= 100) { + if (dispositionModifier != 0 && applyOnlyIfHostile) + { + dispositionModifier + = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + observerStats.modCrimeDispositionModifier(dispositionModifier); + } + startCombat(actor, player); - NpcStats& observerStats = actor.getClass().getNpcStats(actor); // Apply aggression value to the base Fight rating, so that the actor can continue fighting // after a Calm spell wears off observerStats.setAiSetting(AiSetting::Fight, observerFightRating + static_cast(fightTerm)); - observerStats.setBaseDisposition(observerStats.getBaseDisposition() + static_cast(dispTerm)); - - // Set the crime ID, which we will use to calm down participants - // once the bounty has been paid. - observerStats.setCrimeId(id); + setCrimeId = true; // Mark as Alarmed for dialogue observerStats.setAlarmed(true); } } + + // Set the crime ID, which we will use to calm down participants + // once the bounty has been paid and restore their disposition to player character. + if (setCrimeId) + observerStats.setCrimeId(id); } if (reported) diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index b9df650fc3..808059fccd 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -20,6 +20,7 @@ MWMechanics::NpcStats::NpcStats() : mDisposition(0) + , mCrimeDispositionModifier(0) , mReputation(0) , mCrimeId(-1) , mBounty(0) @@ -43,6 +44,21 @@ void MWMechanics::NpcStats::setBaseDisposition(int disposition) mDisposition = disposition; } +int MWMechanics::NpcStats::getCrimeDispositionModifier() const +{ + return mCrimeDispositionModifier; +} + +void MWMechanics::NpcStats::setCrimeDispositionModifier(int value) +{ + mCrimeDispositionModifier = value; +} + +void MWMechanics::NpcStats::modCrimeDispositionModifier(int value) +{ + mCrimeDispositionModifier += value; +} + const MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill(ESM::RefId id) const { auto it = mSkills.find(id); @@ -464,6 +480,7 @@ void MWMechanics::NpcStats::writeState(ESM::NpcStats& state) const state.mFactions[iter->first].mRank = iter->second; state.mDisposition = mDisposition; + state.mCrimeDispositionModifier = mCrimeDispositionModifier; for (const auto& [id, value] : mSkills) { @@ -528,6 +545,7 @@ void MWMechanics::NpcStats::readState(const ESM::NpcStats& state) } mDisposition = state.mDisposition; + mCrimeDispositionModifier = state.mCrimeDispositionModifier; for (size_t i = 0; i < state.mSkills.size(); ++i) { diff --git a/apps/openmw/mwmechanics/npcstats.hpp b/apps/openmw/mwmechanics/npcstats.hpp index b6a655e84f..7113ee6207 100644 --- a/apps/openmw/mwmechanics/npcstats.hpp +++ b/apps/openmw/mwmechanics/npcstats.hpp @@ -22,6 +22,7 @@ namespace MWMechanics class NpcStats : public CreatureStats { int mDisposition; + int mCrimeDispositionModifier; std::map mSkills; // SkillValue.mProgress used by the player only int mReputation; @@ -54,6 +55,10 @@ namespace MWMechanics int getBaseDisposition() const; void setBaseDisposition(int disposition); + int getCrimeDispositionModifier() const; + void setCrimeDispositionModifier(int value); + void modCrimeDispositionModifier(int value); + int getReputation() const; void setReputation(int reputation); diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 687b512106..b8dc047737 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -1346,8 +1346,10 @@ namespace MWScript { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).setBounty(0); - MWBase::Environment::get().getWorld()->confiscateStolenItems(player); - MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + world->confiscateStolenItems(player); + world->getPlayer().recordCrimeId(); + world->getPlayer().setDrawState(MWMechanics::DrawState::Nothing); } }; diff --git a/components/esm3/formatversion.hpp b/components/esm3/formatversion.hpp index 949cdadc38..b460c15247 100644 --- a/components/esm3/formatversion.hpp +++ b/components/esm3/formatversion.hpp @@ -24,7 +24,7 @@ namespace ESM inline constexpr FormatVersion MaxNameIsRefIdOnlyFormatVersion = 25; inline constexpr FormatVersion MaxUseEsmCellIdFormatVersion = 26; inline constexpr FormatVersion MaxActiveSpellSlotIndexFormatVersion = 27; - inline constexpr FormatVersion CurrentSaveGameFormatVersion = 29; + inline constexpr FormatVersion CurrentSaveGameFormatVersion = 30; inline constexpr FormatVersion MinSupportedSaveGameFormatVersion = 4; inline constexpr FormatVersion OpenMW0_48SaveGameFormatVersion = 21; diff --git a/components/esm3/npcstats.cpp b/components/esm3/npcstats.cpp index dc221a5b43..371f506fb4 100644 --- a/components/esm3/npcstats.cpp +++ b/components/esm3/npcstats.cpp @@ -37,6 +37,9 @@ namespace ESM mDisposition = 0; esm.getHNOT(mDisposition, "DISP"); + mCrimeDispositionModifier = 0; + esm.getHNOT(mCrimeDispositionModifier, "DISM"); + const bool intFallback = esm.getFormatVersion() <= MaxIntFallbackFormatVersion; for (auto& skill : mSkills) skill.load(esm, intFallback); @@ -94,6 +97,9 @@ namespace ESM if (mDisposition) esm.writeHNT("DISP", mDisposition); + if (mCrimeDispositionModifier) + esm.writeHNT("DISM", mCrimeDispositionModifier); + for (const auto& skill : mSkills) skill.save(esm); @@ -141,6 +147,7 @@ namespace ESM { mIsWerewolf = false; mDisposition = 0; + mCrimeDispositionModifier = 0; mBounty = 0; mReputation = 0; mWerewolfKills = 0; diff --git a/components/esm3/npcstats.hpp b/components/esm3/npcstats.hpp index 425a62162b..b539602e06 100644 --- a/components/esm3/npcstats.hpp +++ b/components/esm3/npcstats.hpp @@ -33,6 +33,7 @@ namespace ESM std::map mFactions; int32_t mDisposition; + int32_t mCrimeDispositionModifier; std::array, ESM::Skill::Length> mSkills; int32_t mBounty; int32_t mReputation;