You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1016 lines
38 KiB
C++
1016 lines
38 KiB
C++
#include "creature.hpp"
|
|
|
|
#include <components/misc/rng.hpp>
|
|
|
|
#include <components/esm/loadcrea.hpp>
|
|
#include <components/esm/creaturestate.hpp>
|
|
#include <components/settings/settings.hpp>
|
|
|
|
/*
|
|
Start of tes3mp addition
|
|
|
|
Include additional headers for multiplayer purposes
|
|
*/
|
|
#include <components/openmw-mp/Log.hpp>
|
|
#include "../mwmp/Main.hpp"
|
|
#include "../mwmp/LocalPlayer.hpp"
|
|
#include "../mwmp/PlayerList.hpp"
|
|
#include "../mwmp/CellController.hpp"
|
|
#include "../mwmp/MechanicsHelper.hpp"
|
|
/*
|
|
End of tes3mp addition
|
|
*/
|
|
|
|
#include "../mwmechanics/creaturestats.hpp"
|
|
#include "../mwmechanics/magiceffects.hpp"
|
|
#include "../mwmechanics/movement.hpp"
|
|
#include "../mwmechanics/disease.hpp"
|
|
#include "../mwmechanics/spellcasting.hpp"
|
|
#include "../mwmechanics/difficultyscaling.hpp"
|
|
|
|
#include "../mwbase/environment.hpp"
|
|
#include "../mwbase/mechanicsmanager.hpp"
|
|
#include "../mwbase/windowmanager.hpp"
|
|
#include "../mwbase/world.hpp"
|
|
#include "../mwbase/soundmanager.hpp"
|
|
|
|
#include "../mwworld/ptr.hpp"
|
|
#include "../mwworld/actiontalk.hpp"
|
|
#include "../mwworld/actionopen.hpp"
|
|
#include "../mwworld/failedaction.hpp"
|
|
#include "../mwworld/customdata.hpp"
|
|
#include "../mwworld/containerstore.hpp"
|
|
#include "../mwphysics/physicssystem.hpp"
|
|
#include "../mwworld/cellstore.hpp"
|
|
|
|
#include "../mwrender/renderinginterface.hpp"
|
|
#include "../mwrender/objects.hpp"
|
|
|
|
#include "../mwgui/tooltips.hpp"
|
|
|
|
#include "../mwworld/inventorystore.hpp"
|
|
|
|
#include "../mwmechanics/npcstats.hpp"
|
|
#include "../mwmechanics/combat.hpp"
|
|
#include "../mwmechanics/actorutil.hpp"
|
|
|
|
namespace
|
|
{
|
|
bool isFlagBitSet(const MWWorld::ConstPtr &ptr, ESM::Creature::Flags bitMask)
|
|
{
|
|
return (ptr.get<ESM::Creature>()->mBase->mFlags & bitMask) != 0;
|
|
}
|
|
}
|
|
|
|
namespace MWClass
|
|
{
|
|
|
|
class CreatureCustomData : public MWWorld::CustomData
|
|
{
|
|
public:
|
|
MWMechanics::CreatureStats mCreatureStats;
|
|
MWWorld::ContainerStore* mContainerStore; // may be InventoryStore for some creatures
|
|
MWMechanics::Movement mMovement;
|
|
|
|
virtual MWWorld::CustomData *clone() const;
|
|
|
|
virtual CreatureCustomData& asCreatureCustomData()
|
|
{
|
|
return *this;
|
|
}
|
|
virtual const CreatureCustomData& asCreatureCustomData() const
|
|
{
|
|
return *this;
|
|
}
|
|
|
|
CreatureCustomData() : mContainerStore(0) {}
|
|
virtual ~CreatureCustomData() { delete mContainerStore; }
|
|
};
|
|
|
|
MWWorld::CustomData *CreatureCustomData::clone() const
|
|
{
|
|
CreatureCustomData* cloned = new CreatureCustomData (*this);
|
|
cloned->mContainerStore = mContainerStore->clone();
|
|
return cloned;
|
|
}
|
|
|
|
const Creature::GMST& Creature::getGmst()
|
|
{
|
|
static GMST gmst;
|
|
static bool inited = false;
|
|
if (!inited)
|
|
{
|
|
const MWBase::World *world = MWBase::Environment::get().getWorld();
|
|
const MWWorld::Store<ESM::GameSetting> &store = world->getStore().get<ESM::GameSetting>();
|
|
gmst.fMinWalkSpeedCreature = store.find("fMinWalkSpeedCreature");
|
|
gmst.fMaxWalkSpeedCreature = store.find("fMaxWalkSpeedCreature");
|
|
gmst.fEncumberedMoveEffect = store.find("fEncumberedMoveEffect");
|
|
gmst.fSneakSpeedMultiplier = store.find("fSneakSpeedMultiplier");
|
|
gmst.fAthleticsRunBonus = store.find("fAthleticsRunBonus");
|
|
gmst.fBaseRunMultiplier = store.find("fBaseRunMultiplier");
|
|
gmst.fMinFlySpeed = store.find("fMinFlySpeed");
|
|
gmst.fMaxFlySpeed = store.find("fMaxFlySpeed");
|
|
gmst.fSwimRunBase = store.find("fSwimRunBase");
|
|
gmst.fSwimRunAthleticsMult = store.find("fSwimRunAthleticsMult");
|
|
gmst.fKnockDownMult = store.find("fKnockDownMult");
|
|
gmst.iKnockDownOddsMult = store.find("iKnockDownOddsMult");
|
|
gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase");
|
|
inited = true;
|
|
}
|
|
return gmst;
|
|
}
|
|
|
|
void Creature::ensureCustomData (const MWWorld::Ptr& ptr) const
|
|
{
|
|
if (!ptr.getRefData().getCustomData())
|
|
{
|
|
std::unique_ptr<CreatureCustomData> data (new CreatureCustomData);
|
|
|
|
MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
|
|
|
|
// creature stats
|
|
data->mCreatureStats.setAttribute(ESM::Attribute::Strength, ref->mBase->mData.mStrength);
|
|
data->mCreatureStats.setAttribute(ESM::Attribute::Intelligence, ref->mBase->mData.mIntelligence);
|
|
data->mCreatureStats.setAttribute(ESM::Attribute::Willpower, ref->mBase->mData.mWillpower);
|
|
data->mCreatureStats.setAttribute(ESM::Attribute::Agility, ref->mBase->mData.mAgility);
|
|
data->mCreatureStats.setAttribute(ESM::Attribute::Speed, ref->mBase->mData.mSpeed);
|
|
data->mCreatureStats.setAttribute(ESM::Attribute::Endurance, ref->mBase->mData.mEndurance);
|
|
data->mCreatureStats.setAttribute(ESM::Attribute::Personality, ref->mBase->mData.mPersonality);
|
|
data->mCreatureStats.setAttribute(ESM::Attribute::Luck, ref->mBase->mData.mLuck);
|
|
data->mCreatureStats.setHealth(static_cast<float>(ref->mBase->mData.mHealth));
|
|
data->mCreatureStats.setMagicka(static_cast<float>(ref->mBase->mData.mMana));
|
|
data->mCreatureStats.setFatigue(static_cast<float>(ref->mBase->mData.mFatigue));
|
|
|
|
data->mCreatureStats.setLevel(ref->mBase->mData.mLevel);
|
|
|
|
data->mCreatureStats.getAiSequence().fill(ref->mBase->mAiPackage);
|
|
|
|
data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Hello, ref->mBase->mAiData.mHello);
|
|
data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, ref->mBase->mAiData.mFight);
|
|
data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, ref->mBase->mAiData.mFlee);
|
|
data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm);
|
|
|
|
// Persistent actors with 0 health do not play death animation
|
|
if (data->mCreatureStats.isDead())
|
|
data->mCreatureStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr));
|
|
|
|
// spells
|
|
for (std::vector<std::string>::const_iterator iter (ref->mBase->mSpells.mList.begin());
|
|
iter!=ref->mBase->mSpells.mList.end(); ++iter)
|
|
{
|
|
if (const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(*iter))
|
|
data->mCreatureStats.getSpells().add (spell);
|
|
else /// \todo add option to make this a fatal error message pop-up, but default to warning for vanilla compatibility
|
|
std::cerr << "Warning: ignoring nonexistent spell '" << *iter << "' on creature '" << ref->mBase->mId << "'" << std::endl;
|
|
}
|
|
|
|
// inventory
|
|
bool hasInventory = hasInventoryStore(ptr);
|
|
if (hasInventory)
|
|
data->mContainerStore = new MWWorld::InventoryStore();
|
|
else
|
|
data->mContainerStore = new MWWorld::ContainerStore();
|
|
|
|
data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold);
|
|
|
|
data->mCreatureStats.setNeedRecalcDynamicStats(false);
|
|
|
|
// store
|
|
ptr.getRefData().setCustomData(data.release());
|
|
|
|
getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId());
|
|
|
|
if (hasInventory)
|
|
getInventoryStore(ptr).autoEquipShield(ptr);
|
|
}
|
|
}
|
|
|
|
void Creature::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const
|
|
{
|
|
MWRender::Objects& objects = renderingInterface.getObjects();
|
|
objects.insertCreature(ptr, model, hasInventoryStore(ptr));
|
|
}
|
|
|
|
std::string Creature::getModel(const MWWorld::ConstPtr &ptr) const
|
|
{
|
|
const MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
|
|
|
|
const std::string &model = ref->mBase->mModel;
|
|
if (!model.empty()) {
|
|
return "meshes\\" + model;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
void Creature::getModelsToPreload(const MWWorld::Ptr &ptr, std::vector<std::string> &models) const
|
|
{
|
|
std::string model = getModel(ptr);
|
|
if (!model.empty())
|
|
models.push_back(model);
|
|
|
|
// FIXME: use const version of InventoryStore functions once they are available
|
|
if (ptr.getClass().hasInventoryStore(ptr))
|
|
{
|
|
const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr);
|
|
for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot)
|
|
{
|
|
MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot);
|
|
if (equipped != invStore.end())
|
|
{
|
|
model = equipped->getClass().getModel(*equipped);
|
|
if (!model.empty())
|
|
models.push_back(model);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string Creature::getName (const MWWorld::ConstPtr& ptr) const
|
|
{
|
|
const MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
|
|
|
|
return ref->mBase->mName;
|
|
}
|
|
|
|
MWMechanics::CreatureStats& Creature::getCreatureStats (const MWWorld::Ptr& ptr) const
|
|
{
|
|
ensureCustomData (ptr);
|
|
|
|
return ptr.getRefData().getCustomData()->asCreatureCustomData().mCreatureStats;
|
|
}
|
|
|
|
|
|
void Creature::hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const
|
|
{
|
|
/*
|
|
Start of tes3mp addition
|
|
|
|
Ignore hit calculations on this client from DedicatedPlayers and DedicatedActors
|
|
*/
|
|
if (mwmp::PlayerList::isDedicatedPlayer(ptr) || mwmp::Main::get().getCellController()->isDedicatedActor(ptr))
|
|
{
|
|
return;
|
|
}
|
|
/*
|
|
End of tes3mp addition
|
|
*/
|
|
|
|
MWWorld::LiveCellRef<ESM::Creature> *ref =
|
|
ptr.get<ESM::Creature>();
|
|
const MWWorld::Store<ESM::GameSetting> &gmst = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
|
|
MWMechanics::CreatureStats &stats = getCreatureStats(ptr);
|
|
|
|
if (stats.getDrawState() != MWMechanics::DrawState_Weapon)
|
|
return;
|
|
|
|
// Get the weapon used (if hand-to-hand, weapon = inv.end())
|
|
MWWorld::Ptr weapon;
|
|
if (ptr.getClass().hasInventoryStore(ptr))
|
|
{
|
|
MWWorld::InventoryStore &inv = getInventoryStore(ptr);
|
|
MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
|
|
if (weaponslot != inv.end() && weaponslot->getTypeName() == typeid(ESM::Weapon).name())
|
|
weapon = *weaponslot;
|
|
}
|
|
|
|
MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength);
|
|
|
|
float dist = gmst.find("fCombatDistance")->getFloat();
|
|
if (!weapon.isEmpty())
|
|
dist *= weapon.get<ESM::Weapon>()->mBase->mData.mReach;
|
|
|
|
// For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result.
|
|
std::vector<MWWorld::Ptr> targetActors;
|
|
if (!ptr.isEmpty() && ptr.getClass().isActor() && ptr != MWMechanics::getPlayer())
|
|
ptr.getClass().getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors);
|
|
|
|
std::pair<MWWorld::Ptr, osg::Vec3f> result = MWBase::Environment::get().getWorld()->getHitContact(ptr, dist, targetActors);
|
|
if (result.first.isEmpty())
|
|
return; // Didn't hit anything
|
|
|
|
MWWorld::Ptr victim = result.first;
|
|
|
|
if (!victim.getClass().isActor())
|
|
return; // Can't hit non-actors
|
|
|
|
osg::Vec3f hitPosition (result.second);
|
|
|
|
float hitchance = MWMechanics::getHitChance(ptr, victim, ref->mBase->mData.mCombat);
|
|
|
|
/*
|
|
Start of tes3mp addition
|
|
|
|
If the attacker is a LocalPlayer or LocalActor, get their Attack to assign its
|
|
hit position and target
|
|
*/
|
|
mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(ptr);
|
|
|
|
if (localAttack)
|
|
{
|
|
localAttack->isHit = true;
|
|
localAttack->success = true;
|
|
localAttack->hitPosition = MechanicsHelper::getPositionFromVector(hitPosition);
|
|
MechanicsHelper::assignAttackTarget(localAttack, victim);
|
|
}
|
|
/*
|
|
End of tes3mp addition
|
|
*/
|
|
|
|
if(Misc::Rng::roll0to99() >= hitchance)
|
|
{
|
|
/*
|
|
Start of tes3mp addition
|
|
|
|
If this was a failed attack by the LocalPlayer or LocalActor, send a
|
|
packet about it
|
|
*/
|
|
if (localAttack)
|
|
{
|
|
localAttack->pressed = false;
|
|
localAttack->success = false;
|
|
localAttack->shouldSend = true;
|
|
}
|
|
/*
|
|
End of tes3mp addition
|
|
*/
|
|
|
|
victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false);
|
|
MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr);
|
|
return;
|
|
}
|
|
|
|
int min,max;
|
|
switch (type)
|
|
{
|
|
case 0:
|
|
min = ref->mBase->mData.mAttack[0];
|
|
max = ref->mBase->mData.mAttack[1];
|
|
break;
|
|
case 1:
|
|
min = ref->mBase->mData.mAttack[2];
|
|
max = ref->mBase->mData.mAttack[3];
|
|
break;
|
|
case 2:
|
|
default:
|
|
min = ref->mBase->mData.mAttack[4];
|
|
max = ref->mBase->mData.mAttack[5];
|
|
break;
|
|
}
|
|
|
|
float damage = min + (max - min) * attackStrength;
|
|
bool healthdmg = true;
|
|
if (!weapon.isEmpty())
|
|
{
|
|
const unsigned char *attack = NULL;
|
|
if(type == ESM::Weapon::AT_Chop)
|
|
attack = weapon.get<ESM::Weapon>()->mBase->mData.mChop;
|
|
else if(type == ESM::Weapon::AT_Slash)
|
|
attack = weapon.get<ESM::Weapon>()->mBase->mData.mSlash;
|
|
else if(type == ESM::Weapon::AT_Thrust)
|
|
attack = weapon.get<ESM::Weapon>()->mBase->mData.mThrust;
|
|
if(attack)
|
|
{
|
|
damage = attack[0] + ((attack[1]-attack[0])*attackStrength);
|
|
MWMechanics::adjustWeaponDamage(damage, weapon, ptr);
|
|
MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr);
|
|
}
|
|
|
|
// Apply "On hit" enchanted weapons
|
|
|
|
/*
|
|
Start of tes3mp change (minor)
|
|
|
|
Track whether the strike enchantment is successful for attacks by the
|
|
LocalPlayer or LocalActors
|
|
*/
|
|
bool appliedEnchantment = MWMechanics::applyOnStrikeEnchantment(ptr, victim, weapon, hitPosition);
|
|
|
|
if (localAttack)
|
|
localAttack->applyWeaponEnchantment = appliedEnchantment;
|
|
/*
|
|
End of tes3mp change (minor)
|
|
*/
|
|
}
|
|
else if (isBipedal(ptr))
|
|
{
|
|
MWMechanics::getHandToHandDamage(ptr, victim, damage, healthdmg, attackStrength);
|
|
}
|
|
|
|
MWMechanics::applyElementalShields(ptr, victim);
|
|
|
|
if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength))
|
|
damage = 0;
|
|
|
|
if (victim == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState())
|
|
damage = 0;
|
|
|
|
MWMechanics::diseaseContact(victim, ptr);
|
|
|
|
victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true);
|
|
}
|
|
|
|
void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const
|
|
{
|
|
MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
|
|
|
|
// NOTE: 'object' and/or 'attacker' may be empty.
|
|
if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker))
|
|
stats.setAttacked(true);
|
|
|
|
// Self defense
|
|
bool setOnPcHitMe = true; // Note OnPcHitMe is not set for friendly hits.
|
|
|
|
// No retaliation for totally static creatures (they have no movement or attacks anyway)
|
|
if (isMobile(ptr) && !attacker.isEmpty())
|
|
setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker);
|
|
|
|
// Attacker and target store each other as hitattemptactor if they have no one stored yet
|
|
if (!attacker.isEmpty() && attacker.getClass().isActor() && !ptr.isEmpty() && ptr.getClass().isActor())
|
|
{
|
|
MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker);
|
|
|
|
/*
|
|
Start of tes3mp change (minor)
|
|
|
|
Instead of only checking whether an attacker is the LocalPlayer, also
|
|
check if they are a DedicatedPlayer
|
|
*/
|
|
|
|
// First handle the attacked actor
|
|
if ((stats.getHitAttemptActorId() == -1)
|
|
&& (statsAttacker.getAiSequence().isInCombat(ptr)
|
|
|| attacker == MWMechanics::getPlayer()
|
|
|| mwmp::PlayerList::isDedicatedPlayer(attacker)))
|
|
stats.setHitAttemptActorId(statsAttacker.getActorId());
|
|
|
|
// Next handle the attacking actor
|
|
if ((statsAttacker.getHitAttemptActorId() == -1)
|
|
&& (statsAttacker.getAiSequence().isInCombat(ptr)
|
|
|| attacker == MWMechanics::getPlayer()
|
|
|| mwmp::PlayerList::isDedicatedPlayer(attacker)))
|
|
statsAttacker.setHitAttemptActorId(stats.getActorId());
|
|
|
|
/*
|
|
End of tes3mp change (minor)
|
|
*/
|
|
}
|
|
|
|
if (!object.isEmpty())
|
|
stats.setLastHitAttemptObject(object.getCellRef().getRefId());
|
|
|
|
if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer())
|
|
{
|
|
const std::string &script = ptr.get<ESM::Creature>()->mBase->mScript;
|
|
/* Set the OnPCHitMe script variable. The script is responsible for clearing it. */
|
|
if(!script.empty())
|
|
ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1);
|
|
}
|
|
|
|
bool godmode = object == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
|
|
|
|
if (godmode)
|
|
damage = 0;
|
|
|
|
if (!successful)
|
|
{
|
|
// Missed
|
|
MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "miss", 1.0f, 1.0f);
|
|
return;
|
|
}
|
|
|
|
if (!object.isEmpty())
|
|
stats.setLastHitObject(object.getCellRef().getRefId());
|
|
|
|
if (damage > 0.0f && !object.isEmpty())
|
|
MWMechanics::resistNormalWeapon(ptr, attacker, object, damage);
|
|
|
|
if (damage < 0.001f)
|
|
damage = 0;
|
|
|
|
if (damage > 0.f)
|
|
{
|
|
if (!attacker.isEmpty())
|
|
{
|
|
// Check for knockdown
|
|
float agilityTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * getGmst().fKnockDownMult->getFloat();
|
|
float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified()
|
|
* getGmst().iKnockDownOddsMult->getInt() * 0.01f + getGmst().iKnockDownOddsBase->getInt();
|
|
|
|
/*
|
|
Start of tes3mp change (major)
|
|
|
|
If the attacker is a DedicatedPlayer or DedicatedActor with a successful knockdown, apply the knockdown
|
|
|
|
If the attacker is neither of those, then it must be a LocalPlayer or a LocalActor, so calculate the
|
|
knockdown probability on our client
|
|
|
|
Default to hit recovery if no knockdown has taken place, like in regular OpenMW
|
|
*/
|
|
mwmp::Attack *dedicatedAttack = MechanicsHelper::getDedicatedAttack(attacker);
|
|
|
|
if (dedicatedAttack)
|
|
{
|
|
if (dedicatedAttack->knockdown)
|
|
stats.setKnockedDown(true);
|
|
}
|
|
else
|
|
{
|
|
if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99())
|
|
stats.setKnockedDown(true);
|
|
}
|
|
|
|
if (!stats.getKnockedDown())
|
|
stats.setHitRecovery(true); // Is this supposed to always occur?
|
|
/*
|
|
End of tes3mp change (major)
|
|
*/
|
|
}
|
|
|
|
damage = std::max(1.f, damage);
|
|
|
|
if(ishealth)
|
|
{
|
|
if (!attacker.isEmpty() && !godmode)
|
|
{
|
|
damage = scaleDamage(damage, attacker, ptr);
|
|
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
|
|
}
|
|
|
|
MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Health Damage", 1.0f, 1.0f);
|
|
|
|
MWMechanics::DynamicStat<float> health(stats.getHealth());
|
|
health.setCurrent(health.getCurrent() - damage);
|
|
stats.setHealth(health);
|
|
}
|
|
else
|
|
{
|
|
MWMechanics::DynamicStat<float> fatigue(stats.getFatigue());
|
|
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
|
|
stats.setFatigue(fatigue);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Start of tes3mp addition
|
|
|
|
If the attacker was the LocalPlayer or LocalActor, record their target and send a
|
|
packet with it
|
|
|
|
If the victim was a LocalActor who died, record their attacker as the killer
|
|
*/
|
|
mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(attacker);
|
|
|
|
if (localAttack)
|
|
{
|
|
localAttack->pressed = false;
|
|
localAttack->damage = damage;
|
|
localAttack->knockdown = getCreatureStats(ptr).getKnockedDown();
|
|
|
|
MechanicsHelper::assignAttackTarget(localAttack, ptr);
|
|
|
|
localAttack->shouldSend = true;
|
|
}
|
|
|
|
if (mwmp::Main::get().getCellController()->isLocalActor(ptr))
|
|
{
|
|
if (getCreatureStats(ptr).isDead())
|
|
{
|
|
mwmp::Main::get().getCellController()->getLocalActor(ptr)->killer = MechanicsHelper::getTarget(attacker);
|
|
}
|
|
}
|
|
/*
|
|
End of tes3mp addition
|
|
*/
|
|
}
|
|
|
|
std::shared_ptr<MWWorld::Action> Creature::activate (const MWWorld::Ptr& ptr,
|
|
const MWWorld::Ptr& actor) const
|
|
{
|
|
if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf())
|
|
{
|
|
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
|
|
const ESM::Sound *sound = store.get<ESM::Sound>().searchRandom("WolfCreature");
|
|
|
|
std::shared_ptr<MWWorld::Action> action(new MWWorld::FailedAction("#{sWerewolfRefusal}"));
|
|
if(sound) action->setSound(sound->mId);
|
|
|
|
return action;
|
|
}
|
|
|
|
const MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
|
|
|
|
if(stats.isDead())
|
|
{
|
|
bool canLoot = Settings::Manager::getBool ("can loot during death animation", "Game");
|
|
|
|
// by default user can loot friendly actors during death animation
|
|
if (canLoot && !stats.getAiSequence().isInCombat())
|
|
return std::shared_ptr<MWWorld::Action>(new MWWorld::ActionOpen(ptr));
|
|
|
|
// otherwise wait until death animation
|
|
if(stats.isDeathAnimationFinished())
|
|
return std::shared_ptr<MWWorld::Action>(new MWWorld::ActionOpen(ptr));
|
|
|
|
// death animation is not finished, do nothing
|
|
return std::shared_ptr<MWWorld::Action> (new MWWorld::FailedAction(""));
|
|
}
|
|
|
|
if(stats.getAiSequence().isInCombat())
|
|
return std::shared_ptr<MWWorld::Action>(new MWWorld::FailedAction(""));
|
|
|
|
if(stats.getKnockedDown())
|
|
return std::shared_ptr<MWWorld::Action>(new MWWorld::FailedAction(""));
|
|
|
|
return std::shared_ptr<MWWorld::Action>(new MWWorld::ActionTalk(ptr));
|
|
}
|
|
|
|
MWWorld::ContainerStore& Creature::getContainerStore (const MWWorld::Ptr& ptr) const
|
|
{
|
|
ensureCustomData (ptr);
|
|
|
|
return *ptr.getRefData().getCustomData()->asCreatureCustomData().mContainerStore;
|
|
}
|
|
|
|
MWWorld::InventoryStore& Creature::getInventoryStore(const MWWorld::Ptr &ptr) const
|
|
{
|
|
if (hasInventoryStore(ptr))
|
|
return dynamic_cast<MWWorld::InventoryStore&>(getContainerStore(ptr));
|
|
else
|
|
throw std::runtime_error("this creature has no inventory store");
|
|
}
|
|
|
|
bool Creature::hasInventoryStore(const MWWorld::Ptr &ptr) const
|
|
{
|
|
return isFlagBitSet(ptr, ESM::Creature::Weapon);
|
|
}
|
|
|
|
std::string Creature::getScript (const MWWorld::ConstPtr& ptr) const
|
|
{
|
|
const MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
|
|
|
|
return ref->mBase->mScript;
|
|
}
|
|
|
|
bool Creature::isEssential (const MWWorld::ConstPtr& ptr) const
|
|
{
|
|
return isFlagBitSet(ptr, ESM::Creature::Essential);
|
|
}
|
|
|
|
void Creature::registerSelf()
|
|
{
|
|
std::shared_ptr<Class> instance (new Creature);
|
|
|
|
registerClass (typeid (ESM::Creature).name(), instance);
|
|
}
|
|
|
|
float Creature::getSpeed(const MWWorld::Ptr &ptr) const
|
|
{
|
|
MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
|
|
if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead())
|
|
return 0.f;
|
|
|
|
const GMST& gmst = getGmst();
|
|
|
|
float walkSpeed = gmst.fMinWalkSpeedCreature->getFloat() + 0.01f * stats.getAttribute(ESM::Attribute::Speed).getModified()
|
|
* (gmst.fMaxWalkSpeedCreature->getFloat() - gmst.fMinWalkSpeedCreature->getFloat());
|
|
|
|
const MWBase::World *world = MWBase::Environment::get().getWorld();
|
|
const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects();
|
|
|
|
bool running = ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run);
|
|
|
|
// The Run speed difference for creatures comes from the animation speed difference (see runStateToWalkState in character.cpp)
|
|
float runSpeed = walkSpeed;
|
|
|
|
float moveSpeed;
|
|
|
|
if(getEncumbrance(ptr) > getCapacity(ptr))
|
|
moveSpeed = 0.0f;
|
|
else if(canFly(ptr) || (mageffects.get(ESM::MagicEffect::Levitate).getMagnitude() > 0 &&
|
|
world->isLevitationEnabled()))
|
|
{
|
|
float flySpeed = 0.01f*(stats.getAttribute(ESM::Attribute::Speed).getModified() +
|
|
mageffects.get(ESM::MagicEffect::Levitate).getMagnitude());
|
|
flySpeed = gmst.fMinFlySpeed->getFloat() + flySpeed*(gmst.fMaxFlySpeed->getFloat() - gmst.fMinFlySpeed->getFloat());
|
|
const float normalizedEncumbrance = getNormalizedEncumbrance(ptr);
|
|
flySpeed *= 1.0f - gmst.fEncumberedMoveEffect->getFloat() * normalizedEncumbrance;
|
|
flySpeed = std::max(0.0f, flySpeed);
|
|
moveSpeed = flySpeed;
|
|
}
|
|
else if(world->isSwimming(ptr))
|
|
{
|
|
float swimSpeed = walkSpeed;
|
|
if(running)
|
|
swimSpeed = runSpeed;
|
|
swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude();
|
|
swimSpeed *= gmst.fSwimRunBase->getFloat() + 0.01f*getSkill(ptr, ESM::Skill::Athletics) *
|
|
gmst.fSwimRunAthleticsMult->getFloat();
|
|
moveSpeed = swimSpeed;
|
|
}
|
|
else if(running)
|
|
moveSpeed = runSpeed;
|
|
else
|
|
moveSpeed = walkSpeed;
|
|
if(getMovementSettings(ptr).mPosition[0] != 0 && getMovementSettings(ptr).mPosition[1] == 0)
|
|
moveSpeed *= 0.75f;
|
|
|
|
return moveSpeed;
|
|
}
|
|
|
|
MWMechanics::Movement& Creature::getMovementSettings (const MWWorld::Ptr& ptr) const
|
|
{
|
|
ensureCustomData (ptr);
|
|
|
|
return ptr.getRefData().getCustomData()->asCreatureCustomData().mMovement;
|
|
}
|
|
|
|
bool Creature::hasToolTip(const MWWorld::ConstPtr& ptr) const
|
|
{
|
|
if (!ptr.getRefData().getCustomData() || MWBase::Environment::get().getWindowManager()->isGuiMode())
|
|
return true;
|
|
|
|
const CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData();
|
|
|
|
if (customData.mCreatureStats.isDead() && customData.mCreatureStats.isDeathAnimationFinished())
|
|
return true;
|
|
|
|
return !customData.mCreatureStats.getAiSequence().isInCombat();
|
|
}
|
|
|
|
MWGui::ToolTipInfo Creature::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const
|
|
{
|
|
const MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
|
|
|
|
MWGui::ToolTipInfo info;
|
|
info.caption = ref->mBase->mName;
|
|
|
|
std::string text;
|
|
if (MWBase::Environment::get().getWindowManager()->getFullHelp())
|
|
text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script");
|
|
info.text = text;
|
|
|
|
return info;
|
|
}
|
|
|
|
float Creature::getArmorRating (const MWWorld::Ptr& ptr) const
|
|
{
|
|
// Note this is currently unused. Creatures do not use armor mitigation.
|
|
return getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Shield).getMagnitude();
|
|
}
|
|
|
|
float Creature::getCapacity (const MWWorld::Ptr& ptr) const
|
|
{
|
|
const MWMechanics::CreatureStats& stats = getCreatureStats (ptr);
|
|
return static_cast<float>(stats.getAttribute(ESM::Attribute::Strength).getModified() * 5);
|
|
}
|
|
|
|
int Creature::getServices(const MWWorld::ConstPtr &actor) const
|
|
{
|
|
const MWWorld::LiveCellRef<ESM::Creature>* ref = actor.get<ESM::Creature>();
|
|
if (ref->mBase->mHasAI)
|
|
return ref->mBase->mAiData.mServices;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
bool Creature::isPersistent(const MWWorld::ConstPtr &actor) const
|
|
{
|
|
const MWWorld::LiveCellRef<ESM::Creature>* ref = actor.get<ESM::Creature>();
|
|
return ref->mBase->mPersistent;
|
|
}
|
|
|
|
std::string Creature::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const
|
|
{
|
|
const MWWorld::Store<ESM::SoundGenerator> &store = MWBase::Environment::get().getWorld()->getStore().get<ESM::SoundGenerator>();
|
|
|
|
int type = getSndGenTypeFromName(ptr, name);
|
|
if(type >= 0)
|
|
{
|
|
std::vector<const ESM::SoundGenerator*> sounds;
|
|
|
|
MWWorld::LiveCellRef<ESM::Creature>* ref = ptr.get<ESM::Creature>();
|
|
|
|
const std::string& ourId = (ref->mBase->mOriginal.empty()) ? ptr.getCellRef().getRefId() : ref->mBase->mOriginal;
|
|
|
|
MWWorld::Store<ESM::SoundGenerator>::iterator sound = store.begin();
|
|
while(sound != store.end())
|
|
{
|
|
if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(ourId, sound->mCreature)))
|
|
sounds.push_back(&*sound);
|
|
++sound;
|
|
}
|
|
if(!sounds.empty())
|
|
return sounds[Misc::Rng::rollDice(sounds.size())]->mSound;
|
|
}
|
|
|
|
if (type == ESM::SoundGenerator::Land)
|
|
return "Body Fall Large";
|
|
|
|
return "";
|
|
}
|
|
|
|
MWWorld::Ptr Creature::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const
|
|
{
|
|
const MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
|
|
|
|
return MWWorld::Ptr(cell.insert(ref), &cell);
|
|
}
|
|
|
|
bool Creature::isBipedal(const MWWorld::ConstPtr &ptr) const
|
|
{
|
|
return isFlagBitSet(ptr, ESM::Creature::Bipedal);
|
|
}
|
|
|
|
bool Creature::canFly(const MWWorld::ConstPtr &ptr) const
|
|
{
|
|
return isFlagBitSet(ptr, ESM::Creature::Flies);
|
|
}
|
|
|
|
bool Creature::canSwim(const MWWorld::ConstPtr &ptr) const
|
|
{
|
|
return isFlagBitSet(ptr, static_cast<ESM::Creature::Flags>(ESM::Creature::Swims | ESM::Creature::Bipedal));
|
|
}
|
|
|
|
bool Creature::canWalk(const MWWorld::ConstPtr &ptr) const
|
|
{
|
|
return isFlagBitSet(ptr, static_cast<ESM::Creature::Flags>(ESM::Creature::Walks | ESM::Creature::Bipedal));
|
|
}
|
|
|
|
int Creature::getSndGenTypeFromName(const MWWorld::Ptr &ptr, const std::string &name)
|
|
{
|
|
if(name == "left")
|
|
{
|
|
MWBase::World *world = MWBase::Environment::get().getWorld();
|
|
osg::Vec3f pos(ptr.getRefData().getPosition().asVec3());
|
|
if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr))
|
|
return 2;
|
|
if(world->isOnGround(ptr))
|
|
return 0;
|
|
return -1;
|
|
}
|
|
if(name == "right")
|
|
{
|
|
MWBase::World *world = MWBase::Environment::get().getWorld();
|
|
osg::Vec3f pos(ptr.getRefData().getPosition().asVec3());
|
|
if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr))
|
|
return 3;
|
|
if(world->isOnGround(ptr))
|
|
return 1;
|
|
return -1;
|
|
}
|
|
if(name == "swimleft")
|
|
return 2;
|
|
if(name == "swimright")
|
|
return 3;
|
|
if(name == "moan")
|
|
return 4;
|
|
if(name == "roar")
|
|
return 5;
|
|
if(name == "scream")
|
|
return 6;
|
|
if(name == "land")
|
|
return 7;
|
|
|
|
throw std::runtime_error(std::string("Unexpected soundgen type: ")+name);
|
|
}
|
|
|
|
int Creature::getSkill(const MWWorld::Ptr &ptr, int skill) const
|
|
{
|
|
MWWorld::LiveCellRef<ESM::Creature> *ref =
|
|
ptr.get<ESM::Creature>();
|
|
|
|
const ESM::Skill* skillRecord = MWBase::Environment::get().getWorld()->getStore().get<ESM::Skill>().find(skill);
|
|
|
|
switch (skillRecord->mData.mSpecialization)
|
|
{
|
|
case ESM::Class::Combat:
|
|
return ref->mBase->mData.mCombat;
|
|
case ESM::Class::Magic:
|
|
return ref->mBase->mData.mMagic;
|
|
case ESM::Class::Stealth:
|
|
return ref->mBase->mData.mStealth;
|
|
default:
|
|
throw std::runtime_error("invalid specialisation");
|
|
}
|
|
}
|
|
|
|
int Creature::getBloodTexture(const MWWorld::ConstPtr &ptr) const
|
|
{
|
|
int flags = ptr.get<ESM::Creature>()->mBase->mFlags;
|
|
|
|
if (flags & ESM::Creature::Skeleton)
|
|
return 1;
|
|
if (flags & ESM::Creature::Metal)
|
|
return 2;
|
|
return 0;
|
|
}
|
|
|
|
void Creature::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state)
|
|
const
|
|
{
|
|
if (!state.mHasCustomState)
|
|
return;
|
|
|
|
const ESM::CreatureState& state2 = dynamic_cast<const ESM::CreatureState&> (state);
|
|
|
|
if (state.mVersion > 0)
|
|
{
|
|
if (!ptr.getRefData().getCustomData())
|
|
{
|
|
// Create a CustomData, but don't fill it from ESM records (not needed)
|
|
std::unique_ptr<CreatureCustomData> data (new CreatureCustomData);
|
|
|
|
if (hasInventoryStore(ptr))
|
|
data->mContainerStore = new MWWorld::InventoryStore();
|
|
else
|
|
data->mContainerStore = new MWWorld::ContainerStore();
|
|
|
|
ptr.getRefData().setCustomData (data.release());
|
|
}
|
|
}
|
|
else
|
|
ensureCustomData(ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless.
|
|
|
|
CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData();
|
|
|
|
customData.mContainerStore->readState (state2.mInventory);
|
|
customData.mCreatureStats.readState (state2.mCreatureStats);
|
|
}
|
|
|
|
void Creature::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state)
|
|
const
|
|
{
|
|
ESM::CreatureState& state2 = dynamic_cast<ESM::CreatureState&> (state);
|
|
|
|
if (!ptr.getRefData().getCustomData())
|
|
{
|
|
state.mHasCustomState = false;
|
|
return;
|
|
}
|
|
|
|
const CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData();
|
|
|
|
customData.mContainerStore->writeState (state2.mInventory);
|
|
customData.mCreatureStats.writeState (state2.mCreatureStats);
|
|
}
|
|
|
|
int Creature::getBaseGold(const MWWorld::ConstPtr& ptr) const
|
|
{
|
|
return ptr.get<ESM::Creature>()->mBase->mData.mGold;
|
|
}
|
|
|
|
void Creature::respawn(const MWWorld::Ptr &ptr) const
|
|
{
|
|
const MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr);
|
|
if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead())
|
|
return;
|
|
|
|
if (!creatureStats.isDeathAnimationFinished())
|
|
return;
|
|
|
|
const MWWorld::Store<ESM::GameSetting>& gmst = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
|
|
static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->getFloat();
|
|
static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->getFloat();
|
|
|
|
float delay = ptr.getRefData().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay);
|
|
|
|
if (isFlagBitSet(ptr, ESM::Creature::Respawn)
|
|
&& creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp())
|
|
{
|
|
if (ptr.getCellRef().hasContentFile())
|
|
{
|
|
if (ptr.getRefData().getCount() == 0)
|
|
ptr.getRefData().setCount(1);
|
|
|
|
MWBase::Environment::get().getWorld()->removeContainerScripts(ptr);
|
|
ptr.getRefData().setCustomData(NULL);
|
|
|
|
// Reset to original position
|
|
MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0],
|
|
ptr.getCellRef().getPosition().pos[1],
|
|
ptr.getCellRef().getPosition().pos[2]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Creature::restock(const MWWorld::Ptr& ptr) const
|
|
{
|
|
MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
|
|
const ESM::InventoryList& list = ref->mBase->mInventory;
|
|
MWWorld::ContainerStore& store = getContainerStore(ptr);
|
|
store.restock(list, ptr, ptr.getCellRef().getRefId());
|
|
}
|
|
|
|
int Creature::getBaseFightRating(const MWWorld::ConstPtr &ptr) const
|
|
{
|
|
const MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
|
|
return ref->mBase->mAiData.mFight;
|
|
}
|
|
|
|
void Creature::adjustScale(const MWWorld::ConstPtr &ptr, osg::Vec3f &scale, bool /* rendering */) const
|
|
{
|
|
const MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
|
|
scale *= ref->mBase->mScale;
|
|
}
|
|
}
|