diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 3daecbe1c..f7d5d3a1b 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -13,6 +13,7 @@ #include "../mwworld/inventorystore.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/actionequip.hpp" +#include "../mwworld/player.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -208,7 +209,7 @@ namespace MWMechanics && LOS ) { - creatureStats.getAiSequence().stack(AiCombat("player")); + creatureStats.getAiSequence().stack(AiCombat(MWBase::Environment::get().getWorld()->getPlayer().getPlayer())); creatureStats.setHostile(true); } } diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index a0d57e89d..c36c10665 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -4,15 +4,18 @@ #include "../mwworld/class.hpp" #include "../mwworld/timestamp.hpp" -#include "../mwbase/world.hpp" + #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/dialoguemanager.hpp" +#include "character.hpp" +#include "../mwworld/inventorystore.hpp" + #include "creaturestats.hpp" -#include "npcstats.hpp" #include +#include namespace { @@ -22,104 +25,80 @@ namespace return 1.0; return -1.0; } + + void determineAttackType(const MWWorld::Ptr& actor, MWMechanics::Movement &movement); + //chooses an attack depending on probability to avoid uniformity + void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement); } namespace MWMechanics { - - AiCombat::AiCombat(const std::string &targetId) - :mTargetId(targetId),mTimer(0),mTimer2(0) + AiCombat::AiCombat(const MWWorld::Ptr& actor) : + mTarget(actor), + mTimerAttack(0), + mTimerReact(0), + mTimerCombatMove(0), + mCloseUp(false), + mReadyToAttack(false), + mStrike(false), + mCombatMove(false), + mMovement() { } bool AiCombat::execute (const MWWorld::Ptr& actor,float duration) { - if(!MWWorld::Class::get(actor).getCreatureStats(actor).isHostile()) return true; + //General description + if(!actor.getClass().getCreatureStats(actor).isHostile()) + return true; + if(actor.getClass().getCreatureStats(actor).getHealth().getCurrent() <= 0) + return true; - const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr(mTargetId, false); + //Update every frame + if(mReadyToAttack) + determineAttackType(actor, mMovement); - if(MWWorld::Class::get(actor).getCreatureStats(actor).getHealth().getCurrent() <= 0) return true; - - actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); - - if (actor.getClass().hasInventoryStore(actor)) + if(mCombatMove) { - MWMechanics::DrawState_ state = actor.getClass().getCreatureStats(actor).getDrawState(); - if (state == MWMechanics::DrawState_Spell || state == MWMechanics::DrawState_Nothing) - actor.getClass().getCreatureStats(actor).setDrawState(MWMechanics::DrawState_Weapon); - //MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(true); - } - - ESM::Position pos = actor.getRefData().getPosition(); - const ESM::Pathgrid *pathgrid = - MWBase::Environment::get().getWorld()->getStore().get().search(*actor.getCell()->mCell); - - float xCell = 0; - float yCell = 0; - - if (actor.getCell()->mCell->isExterior()) - { - xCell = actor.getCell()->mCell->mData.mX * ESM::Land::REAL_SIZE; - yCell = actor.getCell()->mCell->mData.mY * ESM::Land::REAL_SIZE; - } - - ESM::Pathgrid::Point dest; - dest.mX = target.getRefData().getPosition().pos[0]; - dest.mY = target.getRefData().getPosition().pos[1]; - dest.mZ = target.getRefData().getPosition().pos[2]; - - ESM::Pathgrid::Point start; - start.mX = pos.pos[0]; - start.mY = pos.pos[1]; - start.mZ = pos.pos[2]; - - mTimer2 = mTimer2 + duration; - - if(!mPathFinder.isPathConstructed()) - mPathFinder.buildPath(start, dest, pathgrid, xCell, yCell, true); - else - { - mPathFinder2.buildPath(start, dest, pathgrid, xCell, yCell, true); - ESM::Pathgrid::Point lastPt = mPathFinder.getPath().back(); - if((mTimer2 > 0.25)&&(mPathFinder2.getPathSize() < mPathFinder.getPathSize() || - (dest.mX - lastPt.mX)*(dest.mX - lastPt.mX)+(dest.mY - lastPt.mY)*(dest.mY - lastPt.mY)+(dest.mZ - lastPt.mZ)*(dest.mZ - lastPt.mZ) > 200*200)) + mTimerCombatMove -= duration; + if( mTimerCombatMove <= 0) { - mTimer2 = 0; - mPathFinder = mPathFinder2; + mTimerCombatMove = 0; + mMovement.mPosition[1] = mMovement.mPosition[0] = 0; + mCombatMove = false; } } + actor.getClass().getMovementSettings(actor) = mMovement; + + //actor.getClass().getCreatureStats(actor).setAttackingOrSpell(mReadyToAttack); + mTimerAttack -= duration; + actor.getClass().getCreatureStats(actor).setAttackingOrSpell(mStrike); - mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1],pos.pos[2]); - - float zAngle = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]); - - // TODO: use movement settings instead of rotating directly - MWBase::Environment::get().getWorld()->rotateObject(actor, 0, 0, zAngle, false); - MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 1; - - - float range = 100; - MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(false); - if((dest.mX - start.mX)*(dest.mX - start.mX)+(dest.mY - start.mY)*(dest.mY - start.mY)+(dest.mZ - start.mZ)*(dest.mZ - start.mZ) - < range*range) + float tReaction = 0.25f; + if(mTimerReact < tReaction) { - float directionX = dest.mX - start.mX; - float directionY = dest.mY - start.mY; - float directionResult = sqrt(directionX * directionX + directionY * directionY); + mTimerReact += duration; + return false; + } - zAngle = Ogre::Radian( Ogre::Math::ACos(directionY / directionResult) * sgn(Ogre::Math::ASin(directionX / directionResult)) ).valueDegrees(); - // TODO: use movement settings instead of rotating directly - MWBase::Environment::get().getWorld()->rotateObject(actor, 0, 0, zAngle, false); + //Update with period = tReaction - mPathFinder.clearPath(); + mTimerReact = 0; - if(mTimer == 0) - { - MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(false); - //mTimer = mTimer + duration; - } - if( mTimer > 1) + //actual attacking logic + //TODO: Some skills affect period of strikes.For berserk-like style period ~ 0.25f + float attackPeriod = 1.0f; + if(mReadyToAttack) + { + if(mTimerAttack <= -attackPeriod) { + //TODO: should depend on time between 'start' to 'min attack' + //for better controlling of NPCs' attack strength. + //Also it seems that this time is different for slash/thrust/chop + mTimerAttack = 0.35f * static_cast(rand())/RAND_MAX; + mStrike = true; + + //say a provoking combat phrase if (actor.getClass().isNpc()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); @@ -130,19 +109,240 @@ namespace MWMechanics MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); } } + } + else if (mTimerAttack <= 0) + mStrike = false; + } + else + { + mTimerAttack = -attackPeriod; + mStrike = false; + } + + const MWWorld::Class &cls = actor.getClass(); + const ESM::Weapon *weapon = NULL; + MWMechanics::WeaponType weaptype; + float weapRange, weapSpeed = 1.0f; - MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(true); - mTimer = 0; + actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); + + if(actor.getClass().hasInventoryStore(actor)) + { + MWMechanics::DrawState_ state = actor.getClass().getCreatureStats(actor).getDrawState(); + if (state == MWMechanics::DrawState_Spell || state == MWMechanics::DrawState_Nothing) + actor.getClass().getCreatureStats(actor).setDrawState(MWMechanics::DrawState_Weapon); + + //Get weapon speed and range + MWWorld::ContainerStoreIterator weaponSlot = + MWMechanics::getActiveWeapon(cls.getCreatureStats(actor), cls.getInventoryStore(actor), &weaptype); + if (weaptype == WeapType_HandToHand) + { + const MWWorld::Store &gmst = + MWBase::Environment::get().getWorld()->getStore().get(); + weapRange = gmst.find("fHandToHandReach")->getFloat(); } else { - mTimer = mTimer + duration; + weapon = weaponSlot->get()->mBase; + weapRange = weapon->mData.mReach; + weapSpeed = weapon->mData.mSpeed; + } + weapRange *= 100.0f; + } + else //is creature + { + weaptype = WeapType_HandToHand; //doesn't matter, should only reflect if it is melee or distant weapon + weapRange = 100; //TODO: use true attack range (the same problem in Creature::hit) + } + + //MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(false); + + ESM::Position pos = actor.getRefData().getPosition(); + + float zAngle; + + float rangeMelee; + float rangeCloseUp; + bool distantCombat = false; + if (weaptype==WeapType_BowAndArrow || weaptype==WeapType_Crossbow || weaptype==WeapType_ThowWeapon) // || WeapType_Spell_OnTarget + { + rangeMelee = 1000; // TODO: should depend on archer skill + rangeCloseUp = 0; //doesn't needed when attacking from distance + distantCombat = true; + } + else + { + rangeMelee = weapRange; + rangeCloseUp = 300; + } + + Ogre::Vector3 vStart(pos.pos[0], pos.pos[1], pos.pos[2]); + ESM::Position targetPos = mTarget.getRefData().getPosition(); + Ogre::Vector3 vDest(targetPos.pos[0], targetPos.pos[1], targetPos.pos[2]); + Ogre::Vector3 vDir = vDest - vStart; + float distBetween = vDir.length(); + + if(distBetween < rangeMelee || (distBetween <= rangeCloseUp && mCloseUp) ) + { + //Melee and Close-up combat + vDir.z = 0; + float dirLen = vDir.length(); + zAngle = Ogre::Radian( Ogre::Math::ACos(vDir.y / dirLen) * sgn(Ogre::Math::ASin(vDir.x / dirLen)) ).valueDegrees(); + + // TODO: use movement settings instead of rotating directly + MWBase::Environment::get().getWorld()->rotateObject(actor, 0, 0, zAngle, false); + + if(mPathFinder.isPathConstructed()) + mPathFinder.clearPath(); + + //MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 0; + + //bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor, mTarget); + if (mCloseUp && distBetween > rangeMelee) + { + //Close-up combat: just run up on target + mMovement.mPosition[1] = 1; + } + else + { + //Melee: stop running and attack + mMovement.mPosition[1] = 0; + chooseBestAttack(weapon, mMovement); + + if(mMovement.mPosition[0] != 0 || mMovement.mPosition[1]) + { + mTimerCombatMove = 0.1f + 0.1f * static_cast(rand())/RAND_MAX; + mCombatMove = true; + } + else if(!distantCombat || (distantCombat && rangeMelee/5)) + { + //apply sideway movement (kind of dodging) with some probability + if(static_cast(rand())/RAND_MAX < 0.25) + { + mMovement.mPosition[0] = static_cast(rand())/RAND_MAX < 0.5? 1: -1; + mTimerCombatMove = 0.05f + 0.15f * static_cast(rand())/RAND_MAX; + mCombatMove = true; + } + } + + if(distantCombat && distBetween < rangeMelee/4) + { + mMovement.mPosition[1] = -1; + } + + mReadyToAttack = true; + //only once got in melee combat, actor is allowed to use close-up shortcutting + mCloseUp = true; + } + } + else + { + //target is at far distance: build & follow the path + mCloseUp = false; + + buildNewPath(actor); + + //delete visited path node + mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1],pos.pos[2]); + + zAngle = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]); + + // TODO: use movement settings instead of rotating directly + MWBase::Environment::get().getWorld()->rotateObject(actor, 0, 0, zAngle, false); + //MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 1; + mMovement.mPosition[1] = 1; + mReadyToAttack = false; + } + + if(distBetween > rangeMelee) + { + //special run attack; it shouldn't affect melee combat tactics + if(actor.getClass().getMovementSettings(actor).mPosition[1] == 1) + { + //check if actor can overcome the distance = distToTarget - attackerWeapRange + //less than in time of playing weapon anim from 'start' to 'hit' tags (t_swing) + //then start attacking + float speed1 = cls.getSpeed(actor); + float speed2 = mTarget.getClass().getSpeed(mTarget); + if(mTarget.getClass().getMovementSettings(mTarget).mPosition[0] == 0 + && mTarget.getClass().getMovementSettings(mTarget).mPosition[1] == 0) + speed2 = 0; + + float s1 = distBetween - weapRange; + float t = s1/speed1; + float s2 = speed2 * t; + float t_swing = 0.17f/weapSpeed;//0.17 should be the time of playing weapon anim from 'start' to 'hit' tags + if (t + s2/speed1 <= t_swing) + { + mReadyToAttack = true; + if(mTimerAttack <= -attackPeriod) + { + mTimerAttack = 0.3f*static_cast(rand())/RAND_MAX; + mStrike = true; + } + } + } + } + + return false; + } + + void AiCombat::buildNewPath(const MWWorld::Ptr& actor) + { + //Construct path to target + ESM::Pathgrid::Point dest; + dest.mX = mTarget.getRefData().getPosition().pos[0]; + dest.mY = mTarget.getRefData().getPosition().pos[1]; + dest.mZ = mTarget.getRefData().getPosition().pos[2]; + Ogre::Vector3 newPathTarget = Ogre::Vector3(dest.mX, dest.mY, dest.mZ); + + ESM::Pathgrid::Point lastPt = mPathFinder.getPath().back(); + Ogre::Vector3 currPathTarget(lastPt.mX, lastPt.mY, lastPt.mZ); + float dist = Ogre::Math::Abs((newPathTarget - currPathTarget).length()); + + float targetPosThreshold; + bool isOutside = actor.getCell()->mCell->isExterior(); + if (isOutside) + targetPosThreshold = 300; + else + targetPosThreshold = 100; + + if(dist > targetPosThreshold) + { + //construct new path only if target has moved away more than on + ESM::Position pos = actor.getRefData().getPosition(); + + ESM::Pathgrid::Point start; + start.mX = pos.pos[0]; + start.mY = pos.pos[1]; + start.mZ = pos.pos[2]; + + const ESM::Pathgrid *pathgrid = + MWBase::Environment::get().getWorld()->getStore().get().search(*actor.getCell()->mCell); + + float xCell = 0; + float yCell = 0; + + if (actor.getCell()->mCell->isExterior()) + { + xCell = actor.getCell()->mCell->mData.mX * ESM::Land::REAL_SIZE; + yCell = actor.getCell()->mCell->mData.mY * ESM::Land::REAL_SIZE; } - MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 0; - //MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(!MWWorld::Class::get(actor).getCreatureStats(actor).getAttackingOrSpell()); + if(!mPathFinder.isPathConstructed()) + mPathFinder.buildPath(start, dest, pathgrid, xCell, yCell, isOutside); + else + { + PathFinder newPathFinder; + newPathFinder.buildPath(start, dest, pathgrid, xCell, yCell, isOutside); + + //TO EXPLORE: + //maybe here is a mistake (?): PathFinder::getPathSize() returns number of grid points in the path, + //not the actual path length. Here we should know if the new path is actually more effective. + //if(pathFinder2.getPathSize() < mPathFinder.getPathSize()) + mPathFinder = newPathFinder; + } } - return false; } int AiCombat::getTypeId() const @@ -157,7 +357,7 @@ namespace MWMechanics const std::string &AiCombat::getTargetId() const { - return mTargetId; + return mTarget.getRefData().getHandle(); } AiCombat *MWMechanics::AiCombat::clone() const @@ -166,3 +366,53 @@ namespace MWMechanics } } +namespace +{ +void determineAttackType(const MWWorld::Ptr& actor, MWMechanics::Movement &movement) +{ + if (movement.mPosition[0] && !movement.mPosition[1]) //sideway + actor.getClass().getCreatureStats(actor).setAttackType(MWMechanics::CreatureStats::AT_Slash); + else if (movement.mPosition[1]) //forward + actor.getClass().getCreatureStats(actor).setAttackType(MWMechanics::CreatureStats::AT_Thrust); + else + actor.getClass().getCreatureStats(actor).setAttackType(MWMechanics::CreatureStats::AT_Chop); +} + +void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement) +{ + //the more damage attackType deals the more probability it has + + if (weapon == NULL) + { + //hand-to-hand and creatures' attacks handled here + //hand-to-hand deals equal damage + float roll = static_cast(rand())/RAND_MAX; + if(roll <= 0.333f) //side punch + { + movement.mPosition[0] = (static_cast(rand())/RAND_MAX < 0.5f)? 1: -1; + movement.mPosition[1] = 0; + } + else if(roll <= 0.666f) //forward punch + movement.mPosition[1] = 1; + + return; + } + + int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; + int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; + int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; + + float total = slash + chop + thrust; + + float roll = static_cast(rand())/RAND_MAX; + if(roll <= static_cast(slash)/total) + { + movement.mPosition[0] = (static_cast(rand())/RAND_MAX < 0.5f)? 1: -1; + movement.mPosition[1] = 0; + } + else if(roll <= (static_cast(slash) + static_cast(thrust))/total) + movement.mPosition[1] = 1; + //else chop +} + +} diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 82efc043b..cb9eee973 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -7,12 +7,14 @@ #include "movement.hpp" +#include "../mwbase/world.hpp" + namespace MWMechanics { class AiCombat : public AiPackage { public: - AiCombat(const std::string &targetId); + AiCombat(const MWWorld::Ptr& actor); virtual AiCombat *clone() const; @@ -26,12 +28,22 @@ namespace MWMechanics const std::string &getTargetId() const; private: - std::string mTargetId; - PathFinder mPathFinder; - PathFinder mPathFinder2; - float mTimer; - float mTimer2; + //controls duration of the actual strike + float mTimerAttack; + float mTimerReact; + //controls duration of the sideway & forward moves + //when mCombatMove is true + float mTimerCombatMove; + + bool mReadyToAttack, mStrike; + bool mCloseUp; + bool mCombatMove; + + MWMechanics::Movement mMovement; + MWWorld::Ptr mTarget; + + void buildNewPath(const MWWorld::Ptr& actor); }; } diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 7c2f7472d..ba6cb6449 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -317,7 +317,7 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat } -void CharacterController::getWeaponGroup(WeaponType weaptype, std::string &group) +void getWeaponGroup(WeaponType weaptype, std::string &group) { const WeaponInfo *info = std::find_if(sWeaponTypeList, sWeaponTypeListEnd, FindWeaponType(weaptype)); if(info != sWeaponTypeListEnd) @@ -325,7 +325,7 @@ void CharacterController::getWeaponGroup(WeaponType weaptype, std::string &group } -MWWorld::ContainerStoreIterator CharacterController::getActiveWeapon(CreatureStats &stats, MWWorld::InventoryStore &inv, WeaponType *weaptype) +MWWorld::ContainerStoreIterator getActiveWeapon(CreatureStats &stats, MWWorld::InventoryStore &inv, WeaponType *weaptype) { if(stats.getDrawState() == DrawState_Spell) { diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 33e6cae52..1b73670f9 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -168,12 +168,6 @@ class CharacterController void refreshCurrentAnims(CharacterState idle, CharacterState movement, bool force=false); - static void getWeaponGroup(WeaponType weaptype, std::string &group); - - static MWWorld::ContainerStoreIterator getActiveWeapon(CreatureStats &stats, - MWWorld::InventoryStore &inv, - WeaponType *weaptype); - void clearAnimQueue(); bool updateWeaponState(bool inwater, bool isrunning); @@ -206,6 +200,10 @@ public: void forceStateUpdate(); }; + void getWeaponGroup(WeaponType weaptype, std::string &group); + MWWorld::ContainerStoreIterator getActiveWeapon(CreatureStats &stats, + MWWorld::InventoryStore &inv, + WeaponType *weaptype); } #endif /* GAME_MWMECHANICS_CHARACTER_HPP */ diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index 09b1ed447..759d0ba94 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -432,13 +432,14 @@ namespace MWScript virtual void execute (Interpreter::Runtime &runtime) { MWWorld::Ptr actor = R()(runtime); - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); + std::string targetID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - MWMechanics::CreatureStats& creatureStats = MWWorld::Class::get(actor).getCreatureStats(actor); - creatureStats.getAiSequence().stack(MWMechanics::AiCombat(actorID)); - if (actorID == "player") - creatureStats.setHostile(true); + MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); + + creatureStats.setHostile(true); + creatureStats.getAiSequence().stack( + MWMechanics::AiCombat(MWBase::Environment::get().getWorld()->getPtr(targetID, true) )); } };