Files
combattest/Combat.cpp
T
2026-11-08 07:45:01 -05:00

230 lines
8.0 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));
}
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);
}