bool AiCombat::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { // get or create temporary storage AiCombatStorage& storage = state.get<AiCombatStorage>(); //General description if (actor.getClass().getCreatureStats(actor).isDead()) return true; MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); if (target.isEmpty()) return false; if(!target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered // with the MechanicsManager || target.getClass().getCreatureStats(target).isDead()) return true; if (!storage.isFleeing()) { if (storage.mCurrentAction.get()) // need to wait to init action with its attack range { //Update every frame. UpdateLOS uses a timer, so the LOS check does not happen every frame. updateLOS(actor, target, duration, storage); float targetReachedTolerance = 0.0f; if (storage.mLOS) targetReachedTolerance = storage.mAttackRange; bool is_target_reached = pathTo(actor, target.getRefData().getPosition().pos, duration, targetReachedTolerance); if (is_target_reached) storage.mReadyToAttack = true; } storage.updateCombatMove(duration); if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage); storage.updateAttack(characterController); } else { updateFleeing(actor, target, duration, storage); } storage.mActionCooldown -= duration; float& timerReact = storage.mTimerReact; if (timerReact < AI_REACTION_TIME) { timerReact += duration; } else { timerReact = 0; if (attack(actor, target, storage, characterController)) return true; } return false; }
/* * Current AiCombat movement states (as of 0.29.0), ignoring the details of the * attack states such as CombatMove, Strike and ReadyToAttack: * * +----(within strike range)----->attack--(beyond strike range)-->follow * | | ^ | | * | | | | | * pursue<---(beyond follow range)-----+ +----(within strike range)---+ | * ^ | * | | * +-------------------------(beyond follow range)--------------------+ * * * Below diagram is high level only, the code detail is a little different * (but including those detail will just complicate the diagram w/o adding much) * * +----------(same)-------------->attack---------(same)---------->follow * | |^^ ||| * | ||| ||| * | +--(same)-----------------+|+----------(same)------------+|| * | | | || * | | | (in range) || * | <---+ (too far) | || * pursue<-------------------------[door open]<-----+ || * ^^^ | || * ||| | || * ||+----------evade-----+ | || * || | [closed door] | || * |+----> maybe stuck, check --------------> back up, check door || * | ^ | ^ | ^ || * | | | | | | || * | | +---+ +---+ || * | +-------------------------------------------------------+| * | | * +---------------------------(same)---------------------------------+ * * FIXME: * * The new scheme is way too complicated, should really be implemented as a * proper state machine. * * TODO: * * Use the Observer Pattern to co-ordinate attacks, provide intelligence on * whether the target was hit, etc. */ bool AiCombat::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { // get or create temporary storage AiCombatStorage& storage = state.get<AiCombatStorage>(); //General description if (actor.getClass().getCreatureStats(actor).isDead()) return true; MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); if (target.isEmpty()) return false; if(!target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered // with the MechanicsManager || target.getClass().getCreatureStats(target).isDead()) return true; //Update every frame storage.updateCombatMove(duration); updateActorsMovement(actor, duration, storage.mMovement); storage.updateAttack(characterController); storage.mActionCooldown -= duration; float& timerReact = storage.mTimerReact; if(timerReact < REACTION_INTERVAL) { timerReact += duration; return false; } else { timerReact = 0; return reactionTimeActions(actor, characterController, storage, target); } }
void MWMechanics::AiCombat::updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage) { static const float BLIND_RUN_DURATION = 1.0f; updateLOS(actor, target, duration, storage); AiCombatStorage::FleeState& state = storage.mFleeState; switch (state) { case AiCombatStorage::FleeState_None: return; case AiCombatStorage::FleeState_Idle: { float triggerDist = getMaxAttackDistance(target); if (storage.mLOS && (triggerDist >= 1000 || getDistanceMinusHalfExtents(actor, target) <= triggerDist)) { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*storage.mCell->getCell()); bool runFallback = true; if (pathgrid && !actor.getClass().isPureWaterCreature(actor)) { ESM::Pathgrid::PointList points; CoordinateConverter coords(storage.mCell->getCell()); osg::Vec3f localPos = actor.getRefData().getPosition().asVec3(); coords.toLocal(localPos); int closestPointIndex = PathFinder::GetClosestPoint(pathgrid, localPos); for (int i = 0; i < static_cast<int>(pathgrid->mPoints.size()); i++) { if (i != closestPointIndex && getPathGridGraph(storage.mCell).isPointConnected(closestPointIndex, i)) { points.push_back(pathgrid->mPoints[static_cast<size_t>(i)]); } } if (!points.empty()) { ESM::Pathgrid::Point dest = points[Misc::Rng::rollDice(points.size())]; coords.toWorld(dest); state = AiCombatStorage::FleeState_RunToDestination; storage.mFleeDest = ESM::Pathgrid::Point(dest.mX, dest.mY, dest.mZ); runFallback = false; } } if (runFallback) { state = AiCombatStorage::FleeState_RunBlindly; storage.mFleeBlindRunTimer = 0.0f; } } } break; case AiCombatStorage::FleeState_RunBlindly: { // timer to prevent twitchy movement that can be observed in vanilla MW if (storage.mFleeBlindRunTimer < BLIND_RUN_DURATION) { storage.mFleeBlindRunTimer += duration; storage.mMovement.mRotation[2] = osg::PI + getZAngleToDir(target.getRefData().getPosition().asVec3()-actor.getRefData().getPosition().asVec3()); storage.mMovement.mPosition[1] = 1; updateActorsMovement(actor, duration, storage); } else state = AiCombatStorage::FleeState_Idle; } break; case AiCombatStorage::FleeState_RunToDestination: { static const float fFleeDistance = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fFleeDistance")->mValue.getFloat(); float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length(); if ((dist > fFleeDistance && !storage.mLOS) || pathTo(actor, storage.mFleeDest, duration)) { state = AiCombatStorage::FleeState_Idle; } } break; }; }