238 lines
8.3 KiB
C++
238 lines
8.3 KiB
C++
#include "Combat.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#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<AbilityFired> CharacterCombat::TakeFired() {
|
|
std::vector<AbilityFired> 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));
|
|
}
|
|
|
|
bool CharacterCombat::HasHyperArmor() const {
|
|
if (ability < 0)
|
|
return false;
|
|
const Ability& move = abilities[ability];
|
|
return move.hyperArmor && abilityTime >= AbilityFrameTime(move, move.hyperArmorStartFrame)
|
|
&& (move.hyperArmorEndFrame <= 0.0f || abilityTime < AbilityFrameTime(move, move.hyperArmorEndFrame));
|
|
}
|
|
|
|
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);
|
|
}
|