#include "Combat.h" #include #include "Animator.h" #include "Character.h" #include "Engine.h" #include "Physics.h" // How long an animation is at its own speed, in real seconds, which is what animators play in static float ClipSeconds(const Animation* animation) { if (!animation) return 0.0f; const float ticksPerSecond = animation->GetTicksPerSecond() > 0.0f ? animation->GetTicksPerSecond() : 25.0f; return animation->GetDuration() / ticksPerSecond; } static float ClipSeconds(const AttackMove& move) { return ClipSeconds(move.animation); } // Game seconds a whole ability takes static float AbilityTime(const Ability& ability) { return (ability.duration > 0.0f ? ability.duration : ClipSeconds(ability.animation)) * GetGameTimeScale(); } // Game seconds into an ability one of its frames is static float AbilityFrameTime(const Ability& ability, float frame) { if (ability.lastFrame > 0.0f) return frame / ability.lastFrame * AbilityTime(ability); return frame / 60.0f * GetGameTimeScale(); } unsigned int CharacterCombat::NewSwingId() { static unsigned int nextSwingId = 1; return nextSwingId++; } void CharacterCombat::Reset() { current = -1; next = 0; time = 0.0f; queued = false; ability = -1; queuedAbility = -1; } void CharacterCombat::UseAbility(int index, const glm::vec3& aimPoint) { if (!IsAbilityReady(index)) return; queuedAbility = index; queuedAim = aimPoint; } const Ability* CharacterCombat::GetCurrentAbility() const { return ability >= 0 && ability < (int)abilities.size() ? &abilities[ability] : nullptr; } bool CharacterCombat::IsAbilityReady(int index) const { if (index < 0 || index >= (int)abilities.size()) return false; return index >= (int)cooldowns.size() || cooldowns[index] <= 0.0f; } float CharacterCombat::GetCooldownFraction(int index) const { if (index < 0 || index >= (int)abilities.size() || index >= (int)cooldowns.size()) return 0.0f; const float total = abilities[index].cooldown * GetGameTimeScale(); return total > 0.0f ? glm::clamp(cooldowns[index] / total, 0.0f, 1.0f) : 0.0f; } std::vector CharacterCombat::TakeFired() { std::vector result; result.swap(fired); return result; } bool CharacterCombat::GetAbilityLunge(float& speed) const { if (ability < 0) return false; const Ability& move = abilities[ability]; if (move.lungeDistance <= 0.0f || move.lungeEndFrame <= move.lungeStartFrame) return false; const float start = AbilityFrameTime(move, move.lungeStartFrame); const float end = AbilityFrameTime(move, move.lungeEndFrame); if (abilityTime < start || abilityTime >= end) return false; speed = move.lungeDistance / (end - start); return true; } bool CharacterCombat::IsAbilityPivoting() const { if (ability < 0) return false; const Ability& move = abilities[ability]; return move.pivots && (move.pivotEndFrame <= 0.0f || abilityTime < AbilityFrameTime(move, move.pivotEndFrame)); } void CharacterCombat::StartAbility(CharacterBody& body, int index) { // It takes over the action layer, so a swing in its recovery is over and the combo starts again after current = -1; next = 0; queued = false; ability = index; abilityTime = 0.0f; abilityFired = false; abilityStatusFired = false; abilityLanded = false; abilityUse = NewSwingId(); abilityAim = queuedAim; queuedAbility = -1; const Ability& move = abilities[index]; cooldowns.resize(abilities.size(), 0.0f); cooldowns[index] = move.cooldown * GetGameTimeScale(); const float length = AbilityTime(move); if (move.animation) body.PlayAction(move.animation, length > 0.0f ? ClipSeconds(move.animation) * GetGameTimeScale() / length : 1.0f); } void CharacterCombat::StepAbility(CharacterBody& body, float dt) { // An ability's cooldown only starts running once it's finished, so it can't be chained straight back into itself for (int i = 0; i < (int)cooldowns.size(); i++) if (i != ability) cooldowns[i] = std::max(cooldowns[i] - dt, 0.0f); if (ability >= 0) { abilityTime += dt; const Ability& move = abilities[ability]; const float length = AbilityTime(move); if (!abilityFired && abilityTime >= AbilityFrameTime(move, move.fireFrame)) { abilityFired = true; fired.push_back({ ability, abilityAim }); } if (abilityFired && !abilityStatusFired && move.appliesStatus && move.statusFrame > 0.0f && abilityTime >= AbilityFrameTime(move, move.statusFrame)) { abilityStatusFired = true; fired.push_back({ ability, abilityAim, true }); } // Landing cuts it short at its hitExitFrame, if it has one, and missing at its missExitFrame const bool exitEarly = abilityLanded ? move.hitExitFrame > 0.0f && abilityTime >= AbilityFrameTime(move, move.hitExitFrame) : move.missExitFrame > 0.0f && abilityTime >= AbilityFrameTime(move, move.missExitFrame); if (abilityFired && (abilityTime >= length || exitEarly)) { if (exitEarly && body.actionAnimator && body.actionAnimator->m_CurrentAnimation == move.animation) body.actionAnimator->StopAnimation(); ability = -1; abilityLanded = false; } } // Waits for a swing to reach its recovery, like the next swing in a combo would if (queuedAbility < 0 || ability >= 0) return; if (current >= 0 && time < FrameTime(combo[current], combo[current].recoveryFrame)) return; StartAbility(body, queuedAbility); } // Combat counts in game time like everything else in a character, so real seconds get scaled up to it float CharacterCombat::MoveTime(const AttackMove& move) const { return (move.duration > 0.0f ? move.duration : ClipSeconds(move)) * GetGameTimeScale(); } float CharacterCombat::FrameTime(const AttackMove& move, float frame) const { if (move.lastFrame <= 0.0f) return 0.0f; return frame / move.lastFrame * MoveTime(move); } void CharacterCombat::Start(CharacterBody& body, int index) { current = index; next = (index + 1) % (int)combo.size(); time = 0.0f; queued = false; swingId = NewSwingId(); // Sped up or slowed down to last as long as the move does const AttackMove& move = combo[index]; const float length = MoveTime(move); body.PlayAction(move.animation, length > 0.0f ? ClipSeconds(move) * GetGameTimeScale() / length : 1.0f); // A little step into it if (body.physics && move.forwardVelocity != 0.0f) { body.physics->linearvelocity += body.GetForwardDirection() * move.forwardVelocity; body.physics->WakeUp(); } } void CharacterCombat::Step(CharacterBody& body, float dt) { StepAbility(body, dt); if (combo.empty() || ability >= 0) { queued = false; // no swinging in the middle of an ability return; } if (current >= 0) { time += dt; const AttackMove& move = combo[current]; if (time >= FrameTime(move, move.lastFrame)) { // Let it play all the way out, so the combo starts over current = -1; next = 0; } } if (!queued) return; if (current < 0) { Start(body, next); return; } // Cutting into the recovery carries the combo on. The last swing has to be waited out. const AttackMove& move = combo[current]; const bool last = current == (int)combo.size() - 1; if (!last && time >= FrameTime(move, move.recoveryFrame)) Start(body, next); } const AttackMove* CharacterCombat::GetCurrentMove() const { return current >= 0 && current < (int)combo.size() ? &combo[current] : nullptr; } bool CharacterCombat::IsHitting() const { const AttackMove* move = GetCurrentMove(); return move && time >= FrameTime(*move, move->hitStartFrame) && time <= FrameTime(*move, move->hitEndFrame); }