mirror of
https://github.com/TES3MP/openmw-tes3mp.git
synced 2025-01-15 22:19:54 +00:00
c075496748
Add serverside script functions for determining the killers of both players and actors. Use unsigned ints for script functions returning an object or actor's refNumIndex or mpNum. Remove updateDeadState() from LocalPlayer and make its code part of updateStatsDynamic() for simplicity.
998 lines
38 KiB
C++
998 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 and
|
|
assign data for its target
|
|
*/
|
|
mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(ptr);
|
|
|
|
if (localAttack)
|
|
{
|
|
localAttack->success = true;
|
|
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;
|
|
otherwise, use default probability roll
|
|
*/
|
|
mwmp::Attack *dedicatedAttack = MechanicsHelper::getDedicatedAttack(attacker);
|
|
|
|
if (dedicatedAttack && dedicatedAttack->knockdown)
|
|
stats.setKnockedDown(true);
|
|
else if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99())
|
|
/*
|
|
End of tes3mp change (major)
|
|
*/
|
|
stats.setKnockedDown(true);
|
|
else
|
|
stats.setHitRecovery(true); // Is this supposed to always occur?
|
|
}
|
|
|
|
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 deathReason
|
|
*/
|
|
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);
|
|
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(0).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;
|
|
}
|
|
}
|