Files

286 lines
14 KiB
C++

#ifndef CHARACTER_CLASS
#define CHARACTER_CLASS
#include <vector>
#include <string>
#include <map>
#include <unordered_map>
#include <algorithm>
#include "t.h"
#include "Rig.h"
#include "Object.h"
#include "Animator.h"
#include "CharacterAction.h"
#include "Combat.h"
#include "CharacterTemplate.h"
#include "StatusEffect.h"
class Character;
class CharacterManager;
class CharacterMind;
class Ragdoll;
class Physics;
class p;
// The physical side of a character: where it is, its physics body, its animation,
// and carrying out whatever actions its mind sends it.
class CharacterBody : public t_package {
public:
Rig* rig = nullptr;
p* physics = nullptr; // made by Character::CreatePhysicsBody, its box is t's scale
std::vector<Animator*> animators;
Animator* animator = nullptr; // the base layer, always fully on: standing idle
Animator* walkAnimator = nullptr; // on top of the idles, faded in while walking
Animator* jumpAnimator = nullptr; // on top of walking, played from the start each jump and held while in the air
Animator* actionAnimator = nullptr; // one-shots on top of everything, see PlayAction
CharacterCombat combat; // its attacks, played on the action layer
Animation* flinchAnimation = nullptr; // played on the action layer when it's hit, see Character::Flinch
Animation* knockdownAnimation = nullptr; // played on the action layer when it's knocked down, held at the end
Animation* dashAnimation = nullptr; // played on the action layer while it dashes
float walkSpeed = 1.17f; // m/s on flat ground
float turnSpeed = 12.0f; // how quickly it turns to face where it's walking
float walkFadeSpeed = 8.0f; // how quickly it fades between idle and walking
float jumpSpeed = 10.0f; // m/s straight up when it jumps, about 5 m high
float jumpDelay = 0.0f; // real seconds into the jump animation it actually leaves the ground
float airFadeSpeed = 14.0f; // how quickly the jump fades in on takeoff and out on landing
float sprintMultiplier = 1.4f; // times walkSpeed while sprinting
float sprintAnimationSpeed = 1.25f; // how much faster the walk plays while sprinting
float dashDistance = 4.0f; // m covered by a dash
float dashDelay = 0.0f; // real seconds into the dash animation it starts moving
float dashTime = 0.4f; // real seconds a dash carries it for, after which it's free to do anything again
float dashCooldown = 0.5f; // real seconds after one dash ends before the next
float statusSpeedMultiplier = 1.0f; // from its status effects, set by Character every frame
bool canJump = true; // likewise
CharacterBody() {
animator = AddAnimator(0);
animator->blendWeight = 1.0f; // base always fully visible
}
Animator* AddAnimator(int weight) {
Animator* a = new Animator();
a->weight = weight;
if (rig) a->SetRig(rig);
animators.push_back(a);
std::sort(animators.begin(), animators.end(),
[](Animator* a, Animator* b) { return a->weight < b->weight; });
return a;
}
// The skinning matrices for this frame, from every animator layered together. Worked out in Step.
// Base (weight 0) is the foundation; each higher-weight animator blends in by its blendWeight,
// but only on the bones its animation has tracks for, so an arm-only clip leaves the legs walking.
const std::vector<glm::mat4>& GetBlendedBoneMatrices() const {
return blendedBones.empty() ? animator->GetFinalBoneMatrices() : blendedBones;
}
void SetRig(Rig* r) {
rig = r;
for (Animator* a : animators)
a->SetRig(r);
}
void SetAnimation(Animation* anim) {
animator->SetAnimation(anim);
animator->PlayAnimation();
animator->blendWeight = 1.0f; // base never blends, always instant
}
// Plays an animation once on top of everything else. Starts over if it's already playing.
// speed scales how fast it plays, 2 takes half as long. Holding, it stays on its last pose until stopped.
void PlayAction(Animation* anim, float speed = 1.0f, bool hold = false) {
if (!anim)
return;
if (!actionAnimator) {
actionAnimator = AddAnimator(4);
actionAnimator->blendDuration = 0.1f;
}
actionAnimator->loops = false;
actionAnimator->holdsAtEnd = hold;
actionAnimator->playbackSpeed = speed;
actionAnimator->SetAnimation(anim);
actionAnimator->PlayAnimation();
}
// The loop played while walking. It keeps running underneath so it picks up mid-stride.
void SetWalkAnimation(Animation* anim) {
if (!walkAnimator)
walkAnimator = AddAnimator(2);
walkAnimator->loops = true;
walkAnimator->SetAnimation(anim);
walkAnimator->PlayAnimation();
walkAnimator->blendWeight = walkBlend;
}
// The clip for jumping. It plays once from takeoff and holds its last pose until it lands.
void SetJumpAnimation(Animation* anim) {
if (!jumpAnimator)
jumpAnimator = AddAnimator(3);
jumpAnimator->loops = false;
jumpAnimator->holdsAtEnd = true;
jumpAnimator->SetAnimation(anim);
jumpAnimator->isPlaying = false; // until it jumps
jumpAnimator->blendWeight = 0.0f;
}
bool IsInAir() const { return inAir; }
bool IsDashing() const { return dashWindup > 0.0f || dashLeft > 0.0f; }
bool IsSprinting() const { return sprinting; }
// Stops a dash where it is, like when it's hit
void CancelDash();
void StopDashAnimation();
// Carries out one action. Movement actions only count for the frame they arrive in.
void Perform(const CharacterAction& action);
// Pushes the physics body toward walking speed along wherever it faces, from this frame's Move action.
// speedFactor scales it, 0 stops it. The walk animation plays faster or slower to match.
void ApplyMovement(bool grounded, float speedFactor, float dt, float referenceDt);
// What this frame's Move action asked for, until ApplyMovement uses it up
bool IsTryingToMove() const { return glm::length(wishDirection) > 0.0f; }
// Turning and animation
void Step(float dt);
void SetWalking(bool walking);
glm::vec3 GetForwardDirection() const;
// Turns to face along direction, flattened. Walking turns it too, this is for while it stands still.
// Instantly snaps round rather than turning over a few frames, for things that have to go the right way at once.
void Face(const glm::vec3& direction, bool instantly = false);
private:
glm::vec3 wishDirection = glm::vec3(0.0f); // from this frame's Move action
bool strafing = false; // facing somewhere it was told to, walking any way
glm::vec3 turnDirection = glm::vec3(0.0f); // where it's turning to face, kept after it stops
bool walking = false;
float walkBlend = 0.0f; // 0 = idle, 1 = walking
bool jumpWanted = false; // from this frame's Jump action, until ApplyMovement uses it up
bool inAir = false;
float takeoffTime = 0.0f; // seconds it still counts as in the air after jumping, while the ground check catches up
float windup = -1.0f; // game seconds until it leaves the ground, below 0 when it isn't about to jump
float airBlend = 0.0f; // 0 = on the ground, 1 = the jump fully showing
bool sprinting = false; // from this frame's Move action
float dashWindup = 0.0f; // game seconds until the dash starts moving it
float dashLeft = 0.0f; // game seconds of dash left, 0 when it isn't dashing
float dashCooldownLeft = 0.0f; // game seconds until it can dash again
glm::vec3 dashDirection = glm::vec3(0.0f);
std::vector<glm::mat4> blendedBones;
// Per (base clip, layer clip): bone name -> what turns the layer's track into one relative to the bone's
// real parent. Only bones whose parent differs between the two files have an entry.
std::map<std::pair<const Animation*, const Animation*>, std::unordered_map<std::string, glm::mat4>> parentCorrections;
void BlendBones(const AssimpNodeData& node, const glm::mat4& parentTransform);
const std::unordered_map<std::string, glm::mat4>& GetParentCorrections(const Animation* layer);
};
class Character : public Object, public Renderable {
public:
CharacterMind* mind = nullptr; // what decides its actions, nullptr does nothing
CharacterBody* body = nullptr;
const CharacterTemplate* characterTemplate = nullptr; // the kind of fighter it is, see ApplyTemplate
std::string displayName; // shown over its head and in menus, its name when empty
Ragdoll* ragdoll = nullptr; // when set, physics drives the rig instead of the animators
SurfaceTexture surface;
glm::vec4 colorOverride = glm::vec4(0.0f); // rgb + strength (0 = keep the rig's own texture)
float visualScale = 1.4f; // rig is ~1.8 tall natively; its physics box is 2.5
float metallic = 0.0f;
float roughness = 0.7f;
float ao = 1.0f;
float maxHealth = 100.0f;
bool healsOnDeath = false; // dying puts it straight back to full health instead
float GetHealth() const { return health; }
int GetDeaths() const { return deaths; } // how many times it's gone down, so things on it can tell it respawned
bool IsDead() const { return dead; }
// Takes away health, dying at 0. Returns how much it actually lost.
float TakeDamage(float amount);
// Gives back health up to maxHealth, nothing for the dead. Returns how much it actually gained.
float Heal(float amount);
// Can't do anything for seconds of real time: no moving, turning, attacking or jumping, and whatever move it
// was in is cut off. A longer stun replaces a shorter one, a shorter one does nothing.
void Stun(float seconds);
bool IsStunned() const { return stunLeft > 0.0f; }
// Puts a status effect on it for seconds of real time, below 0 forever. Already on, the longer of the two stays.
void AddStatus(StatusEffect::Type type, float seconds = -1.0f);
void RemoveStatus(StatusEffect::Type type);
bool HasStatus(StatusEffect::Type type) const;
// Knocked to the floor for seconds of real time, unable to do anything. Only hits that hit the knocked down land.
void Knockdown(float seconds);
bool IsKnockedDown() const { return HasStatus(StatusEffect::Type::Knockdown); }
bool CanAct() const; // not stunned or held by a status effect
bool DodgesHits() const; // a status effect keeps ordinary hits off it
bool IsInvulnerable() const; // has iframes: a status effect keeps every hit off it
bool HasHyperArmor() const; // in a move that takes hits without flinching, being stunned or pushed about
const std::vector<StatusEffect>& GetStatuses() const { return statuses; }
// Recoils from a hit: whatever swing it was in is cut off and its flinch plays over everything, if it has one.
// Nothing for the dead.
void Flinch();
// Goes down, stopping where it is, until it's revived, which the game does straight away somewhere else.
// One that heals on death just goes back to full health where it is.
void Die();
// Back on its feet at full health, wherever its body box is
void Revive();
// Asks the mind for this frame's actions and hands them to the body. The dead don't think.
void Think();
// Moves the body from this frame's actions, slower while it's attacking
void ApplyMovement(bool grounded, float dt, float referenceDt);
glm::vec3 GetForwardDirection() const { return body->GetForwardDirection(); }
const std::string& GetDisplayName() const { return displayName.empty() ? name : displayName; }
// Where a bone of the clip playing on the action layer is in the world this frame, rotation and all.
// Works for bones only that clip has, like a weapon bone. False unless animation is what's playing there.
bool GetActionBoneTransform(const Animation* animation, const std::string& bone, glm::mat4& transform) const;
// Where a bone is in the world this frame with every layer blended in, rotation and all.
// False for a bone the rig doesn't have, or while it's a ragdoll.
bool GetBoneTransform(const std::string& bone, glm::mat4& transform) const;
void EnableRagdoll(Physics& physics);
// Becomes that kind of fighter: its rig, animations, combo and stats, at full health unless it's dead.
// Its looks, name and physics body are left alone.
void ApplyTemplate(const CharacterTemplate& characterTemplate);
// The box physics moves the character around with, sized by body->t's scale
void CreatePhysicsBody(Physics& physics, float massKg);
// Its physics body's group, so it never collides with anything else of its own
int GetCollisionGroup();
Character();
void Render(Shader& shader) override;
bool GetWorldBounds(BoundingBox& bounds) const override;
void Step(float dt);
private:
glm::mat4 GetRigMatrix() const;
float health = 100.0f;
int deaths = 0;
float stunLeft = 0.0f; // game seconds
std::vector<StatusEffect> statuses;
bool wasKnockedDown = false; // last frame, so getting up stops the held pose
bool dead = false;
Physics* world = nullptr; // the physics its body is in, for EnableRagdoll
int collisionGroup = 0;
std::vector<CharacterAction> pendingActions;
};
#endif