finished the game (i will probably not be touching this again)
This commit is contained in:
@@ -434,6 +434,17 @@ bool Character::CanAct() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Character::IsInvulnerable() const {
|
||||
for (const StatusEffect& status : statuses)
|
||||
if (GetStatusEffectInfo(status.type).invulnerable)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Character::HasHyperArmor() const {
|
||||
return body && body->combat.HasHyperArmor();
|
||||
}
|
||||
|
||||
bool Character::DodgesHits() const {
|
||||
for (const StatusEffect& status : statuses)
|
||||
if (GetStatusEffectInfo(status.type).dodgesHits)
|
||||
|
||||
@@ -220,6 +220,8 @@ public:
|
||||
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.
|
||||
|
||||
@@ -96,6 +96,14 @@ bool CharacterCombat::IsAbilityPivoting() const {
|
||||
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;
|
||||
|
||||
@@ -53,6 +53,10 @@ struct Ability {
|
||||
float moveSpeedMultiplier = 0.5f; // of walking speed while it's going
|
||||
bool pivots = false; // keeps turning to face where the player looks while it's going, so it can be aimed
|
||||
float pivotEndFrame = 0.0f; // stops pivoting here, 0 pivots all the way through
|
||||
// Hyper armor: hits still hurt, but don't flinch, stun, knock back or knock down the user, so the move carries on
|
||||
bool hyperArmor = false;
|
||||
float hyperArmorStartFrame = 0.0f;
|
||||
float hyperArmorEndFrame = 0.0f; // 0 keeps it to the end
|
||||
std::string sound; // played where it goes off, as it does. Empty plays nothing.
|
||||
float screenshake = 0.0f; // how hard the camera shakes as it goes off, when it's the player's
|
||||
|
||||
@@ -142,6 +146,9 @@ public:
|
||||
// Whether the ability it's in turns it to face where it's aimed right now
|
||||
bool IsAbilityPivoting() const;
|
||||
|
||||
// Whether the ability it's in has hyper armor right now
|
||||
bool HasHyperArmor() const;
|
||||
|
||||
// Whether abilities[index] can be used now, and how much of its cooldown is left, 1 just used to 0 ready
|
||||
bool IsAbilityReady(int index) const;
|
||||
float GetCooldownFraction(int index) const;
|
||||
|
||||
+105
-8
@@ -226,7 +226,7 @@ void Engine::Initiate()
|
||||
shoot.knockback = 1.5f;
|
||||
shoot.stun = 0.8f;
|
||||
shoot.sound = "assets/gunshot.mp3";
|
||||
shoot.screenshake = 0.45f; // a punch landing is 0.3, the combo finisher 0.6
|
||||
shoot.screenshake = 0.65f; // a punch landing is 0.3, the combo finisher 0.6
|
||||
if (std::filesystem::exists("assets/rifleexecutionhit.glb")) // whoever it kills goes down like this, filmed by its Camera bone
|
||||
shoot.killAnimation = charactermanager->LoadAnimation("rifleexecutionhit", "assets/rifleexecutionhit.glb", "humanoid");
|
||||
shoot.killSounds = { { 40.0f, "assets/dramaticwoosh.mp3" }, { 60.0f, "assets/glassbreak.mp3" } };
|
||||
@@ -238,7 +238,7 @@ void Engine::Initiate()
|
||||
// 2: a switchblade out of nowhere into whoever's in front. It stays in them a while, bleeding them.
|
||||
Ability stab;
|
||||
stab.name = "Stab";
|
||||
stab.description = "Pull a switchblade and stab whoever is right in front of you. The knife stays in them, making them bleed. Stuns what it hits.";
|
||||
stab.description = "Pull a switchblade and stab whoever is right in front of you. The knife stays in them, making them bleed. Stuns what it hits. Hyper armor: hits don't stop it.";
|
||||
stab.effect = Ability::Effect::Stab;
|
||||
stab.animation = charactermanager->LoadAnimation("riflestab", "assets/riflestab.glb", "humanoid"); // brings the Knife bone
|
||||
stab.lastFrame = 60.0f; // 1 s at 60 fps, played at its own speed
|
||||
@@ -246,6 +246,7 @@ void Engine::Initiate()
|
||||
stab.hitExitFrame = 50.0f; // landing it, it's over 10 frames early
|
||||
stab.cooldown = 3.0f;
|
||||
stab.moveSpeedMultiplier = 0.0f;
|
||||
stab.hyperArmor = true; // trades hits rather than being knocked out of it
|
||||
stab.lungeDistance = 1.5f; // steps in over the first 15 frames
|
||||
stab.lungeStartFrame = 0.0f;
|
||||
stab.lungeEndFrame = 15.0f;
|
||||
@@ -481,7 +482,14 @@ void Engine::Initiate()
|
||||
// The camera shakes by shake when it was the player's.
|
||||
auto LandHit = [&](const Character* attacker, Character& target, float damage, const glm::vec3& shove, float shake,
|
||||
float stun) {
|
||||
// Hyper armor takes the damage and nothing else
|
||||
const bool armored = target.HasHyperArmor();
|
||||
Hurt(target, damage);
|
||||
if (armored && !target.IsDead()) {
|
||||
if (attacker == controlled)
|
||||
camera->AddScreenshake(shake);
|
||||
return;
|
||||
}
|
||||
target.Flinch(); // nothing if that was the hit that put them down
|
||||
target.Stun(stun);
|
||||
if (target.ragdoll) {
|
||||
@@ -968,7 +976,8 @@ void Engine::Initiate()
|
||||
float life = 0.0f; // real seconds left, 0 when it's free
|
||||
};
|
||||
std::vector<Tracer> tracers;
|
||||
const float tracerTime = 0.06f;
|
||||
const float tracerTime = 0.16f; // real seconds, thinning out to nothing over it
|
||||
const float tracerThickness = 0.07f;
|
||||
|
||||
auto ShowTracer = [&](const glm::vec3& from, const glm::vec3& direction, float length) {
|
||||
Tracer* tracer = nullptr;
|
||||
@@ -979,12 +988,12 @@ void Engine::Initiate()
|
||||
tracer = &tracers.back();
|
||||
tracer->mesh = CreateCubeMesh();
|
||||
tracer->mesh->name = "tracer";
|
||||
tracer->mesh->colorOverride = glm::vec4(1.0f, 0.86f, 0.55f, 1.0f);
|
||||
tracer->mesh->colorOverride = glm::vec4(1.0f, 0.82f, 0.45f, 1.0f);
|
||||
tracer->mesh->emissive = 1.0f;
|
||||
tracer->mesh->castshadow = false;
|
||||
}
|
||||
// A thin box from end to end, its length along its own z
|
||||
tracer->mesh->t.ScaleTo({ 0.025f, 0.025f, length });
|
||||
tracer->mesh->t.ScaleTo({ tracerThickness, tracerThickness, length });
|
||||
tracer->mesh->t.RotateToQuaternion(LookAt(direction));
|
||||
tracer->mesh->t.TranslateTo(from + direction * (length * 0.5f));
|
||||
if (tracer->life <= 0.0f)
|
||||
@@ -1004,10 +1013,66 @@ void Engine::Initiate()
|
||||
muzzleSparks->emitangle = glm::vec3(0.7f, 0.7f, 0.0f); // radians of spread
|
||||
muzzleSparks->facecamera = true;
|
||||
muzzleSparks->BindToRenderSystem();
|
||||
const int sparksPerShot = 14;
|
||||
const int sparksPerShot = 32;
|
||||
|
||||
// The flash itself: a few big bright blobs right on the muzzle for a frame or two
|
||||
ParticleEmitter* muzzleFlash = new ParticleEmitter();
|
||||
muzzleFlash->name = "muzzle flash";
|
||||
muzzleFlash->surface = SurfaceTexture::Stretched(blanktex);
|
||||
muzzleFlash->opaque = false;
|
||||
muzzleFlash->color = glm::vec4(3.0f, 2.2f, 1.0f, 1.0f);
|
||||
muzzleFlash->size = glm::vec3(0.32f);
|
||||
muzzleFlash->speed = 2.5f;
|
||||
muzzleFlash->lifespan = 0.05f;
|
||||
muzzleFlash->emitangle = glm::vec3(0.5f, 0.5f, 0.0f);
|
||||
muzzleFlash->facecamera = true;
|
||||
muzzleFlash->BindToRenderSystem();
|
||||
const int flashesPerShot = 6;
|
||||
|
||||
// Smoke hanging about the barrel afterwards, drifting up
|
||||
ParticleEmitter* muzzleSmoke = new ParticleEmitter();
|
||||
muzzleSmoke->name = "muzzle smoke";
|
||||
muzzleSmoke->surface = SurfaceTexture::Stretched(blanktex);
|
||||
muzzleSmoke->opaque = false;
|
||||
muzzleSmoke->color = glm::vec4(0.55f, 0.55f, 0.55f, 0.35f);
|
||||
muzzleSmoke->size = glm::vec3(0.22f);
|
||||
muzzleSmoke->speed = 1.2f;
|
||||
muzzleSmoke->lifespan = 0.7f;
|
||||
muzzleSmoke->emitangle = glm::vec3(1.2f, 1.2f, 0.0f);
|
||||
muzzleSmoke->gravity = glm::vec3(0.0f, 1.5f, 0.0f);
|
||||
muzzleSmoke->facecamera = true;
|
||||
muzzleSmoke->BindToRenderSystem();
|
||||
const int smokePerShot = 8;
|
||||
|
||||
// Sparks off whoever it hits, back toward the shooter
|
||||
ParticleEmitter* impactSparks = new ParticleEmitter();
|
||||
impactSparks->name = "impact sparks";
|
||||
impactSparks->surface = SurfaceTexture::Stretched(blanktex);
|
||||
impactSparks->opaque = false;
|
||||
impactSparks->color = glm::vec4(2.0f, 1.4f, 0.6f, 1.0f);
|
||||
impactSparks->size = glm::vec3(0.05f);
|
||||
impactSparks->speed = 6.0f;
|
||||
impactSparks->lifespan = 0.2f;
|
||||
impactSparks->emitangle = glm::vec3(1.6f, 1.6f, 0.0f);
|
||||
impactSparks->gravity = glm::vec3(0.0f, -12.0f, 0.0f);
|
||||
impactSparks->facecamera = true;
|
||||
impactSparks->BindToRenderSystem();
|
||||
const int impactSparksPerHit = 24;
|
||||
|
||||
// Lights up everything around the gun for a moment
|
||||
PointLight* muzzleLight = rendersystem->lighting->AddPointLight(glm::vec3(0.0f), glm::vec3(1.0f, 0.7f, 0.35f), 0.0f, 8.0f);
|
||||
muzzleLight->castShadow = false; // too quick to be worth six shadow renders a frame
|
||||
const float muzzleLightIntensity = 160.0f;
|
||||
const float muzzleLightTime = 0.08f; // real seconds
|
||||
float muzzleLightLeft = 0.0f;
|
||||
|
||||
auto UpdateTracers = [&](float dt) {
|
||||
muzzleSparks->Step(dt);
|
||||
muzzleFlash->Step(dt);
|
||||
muzzleSmoke->Step(dt);
|
||||
impactSparks->Step(dt);
|
||||
muzzleLightLeft = std::max(muzzleLightLeft - dt, 0.0f);
|
||||
muzzleLight->intensity = muzzleLightIntensity * (muzzleLightLeft / muzzleLightTime);
|
||||
for (Tracer& tracer : tracers) {
|
||||
if (tracer.life <= 0.0f)
|
||||
continue;
|
||||
@@ -1015,7 +1080,11 @@ void Engine::Initiate()
|
||||
if (tracer.life <= 0.0f) {
|
||||
tracer.life = 0.0f;
|
||||
rendersystem->RemoveRenderable(tracer.mesh);
|
||||
continue;
|
||||
}
|
||||
const float thickness = tracerThickness * (tracer.life / tracerTime);
|
||||
const glm::vec3 scale = tracer.mesh->t.GetScale();
|
||||
tracer.mesh->t.ScaleTo({ thickness, thickness, scale.z });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1042,6 +1111,13 @@ void Engine::Initiate()
|
||||
shot.stun = ability.stun;
|
||||
shot.killAnimation = ability.killAnimation;
|
||||
shot.killAbility = &ability;
|
||||
shot.onHit = [&, direction](Character& target) {
|
||||
const glm::vec3 at = target.body->t.GetTranslation() + glm::vec3(0.0f, target.body->t.GetScale().y * 0.2f, 0.0f);
|
||||
impactSparks->t.TranslateTo(at);
|
||||
impactSparks->t.RotateToQuaternion(LookAt(-direction));
|
||||
for (int i = 0; i < impactSparksPerHit; i++)
|
||||
impactSparks->Emit();
|
||||
};
|
||||
hitboxsystem->Add(shot);
|
||||
ShowTracer(origin, direction, ability.range);
|
||||
|
||||
@@ -1052,6 +1128,16 @@ void Engine::Initiate()
|
||||
muzzleSparks->t.RotateToQuaternion(LookAt(direction));
|
||||
for (int i = 0; i < sparksPerShot; i++)
|
||||
muzzleSparks->Emit();
|
||||
muzzleFlash->t.TranslateTo(origin + direction * 0.15f);
|
||||
muzzleFlash->t.RotateToQuaternion(LookAt(direction));
|
||||
for (int i = 0; i < flashesPerShot; i++)
|
||||
muzzleFlash->Emit();
|
||||
muzzleSmoke->t.TranslateTo(origin);
|
||||
muzzleSmoke->t.RotateToQuaternion(LookAt(direction));
|
||||
for (int i = 0; i < smokePerShot; i++)
|
||||
muzzleSmoke->Emit();
|
||||
muzzleLight->position = origin + direction * 0.3f;
|
||||
muzzleLightLeft = muzzleLightTime;
|
||||
|
||||
// Its own channel every time, so quick shots overlap rather than cutting each other off
|
||||
if (!ability.sound.empty())
|
||||
@@ -1179,7 +1265,7 @@ void Engine::Initiate()
|
||||
auto ApplyAbilityStatus = [&](Character& user, const Ability& ability) {
|
||||
const unsigned int use = user.body->combat.GetAbilityUse();
|
||||
for (const AbilityHit& hit : abilityHits)
|
||||
if (hit.user == &user && hit.use == use && !hit.target->IsDead())
|
||||
if (hit.user == &user && hit.use == use && !hit.target->IsDead() && !hit.target->IsInvulnerable())
|
||||
hit.target->AddStatus(ability.status, ability.statusTime);
|
||||
};
|
||||
|
||||
@@ -1490,6 +1576,7 @@ void Engine::Initiate()
|
||||
// ------------------------------------------------------------------------
|
||||
struct Execution {
|
||||
Character* victim = nullptr;
|
||||
Character* killer = nullptr; // has iframes until it's over, so the cutscene can't be cut short
|
||||
Animation* animation = nullptr;
|
||||
float timeLeft = 0.0f; // real seconds
|
||||
float time = 0.0f; // real seconds since it started
|
||||
@@ -1526,6 +1613,9 @@ void Engine::Initiate()
|
||||
const float ticksPerSecond = animation->GetTicksPerSecond() > 0.0f ? animation->GetTicksPerSecond() : 25.0f;
|
||||
execution.timeLeft = animation->GetDuration() / ticksPerSecond;
|
||||
execution.filmed = (&victim == controlled || killer == controlled) && !IsFilming();
|
||||
execution.killer = const_cast<Character*>(killer);
|
||||
if (execution.killer && execution.killer != &victim)
|
||||
execution.killer->AddStatus(StatusEffect::Type::Invulnerable);
|
||||
if (ability && execution.filmed)
|
||||
execution.sounds = ability->killSounds;
|
||||
executions.push_back(execution);
|
||||
@@ -1560,6 +1650,13 @@ void Engine::Initiate()
|
||||
|
||||
// Over: up again somewhere else, or for those that heal on death, right where they are
|
||||
victim.RemoveStatus(StatusEffect::Type::Executed);
|
||||
if (Character* killer = execution.killer) {
|
||||
// Unless it's in another one of its own
|
||||
const bool stillExecuting = std::any_of(executions.begin(), executions.end(), [&](const Execution& other) {
|
||||
return &other != &execution && other.killer == killer; });
|
||||
if (!stillExecuting)
|
||||
killer->RemoveStatus(StatusEffect::Type::Invulnerable);
|
||||
}
|
||||
if (Animator* action = victim.body->actionAnimator; action && action->m_CurrentAnimation == execution.animation) {
|
||||
action->holdsAtEnd = false;
|
||||
action->StopAnimation();
|
||||
@@ -1865,7 +1962,7 @@ void Engine::Initiate()
|
||||
StartExecution(target, attacker, hitbox.killAnimation, hitbox.killAbility);
|
||||
return;
|
||||
}
|
||||
if (hitbox.knockdown > 0.0f)
|
||||
if (hitbox.knockdown > 0.0f && !target.HasHyperArmor())
|
||||
target.Knockdown(hitbox.knockdown);
|
||||
if (hitbox.onHit)
|
||||
hitbox.onHit(target);
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ void HitboxSystem::Resolve(const std::vector<Character*>& characters, const HitH
|
||||
for (Character* target : characters) {
|
||||
if (target == hitbox.owner || target->IsDead() || target->ragdoll || !target->body)
|
||||
continue;
|
||||
if (target->DodgesHits() && !hitbox.hitsKnockedDown)
|
||||
if (target->IsInvulnerable() || (target->DodgesHits() && !hitbox.hitsKnockedDown))
|
||||
continue;
|
||||
if (hitbox.swing != 0) {
|
||||
const std::vector<const Character*>& already = hitBySwing[hitbox.swing];
|
||||
|
||||
+7
-2
@@ -8,7 +8,8 @@ struct StatusEffect {
|
||||
enum class Type {
|
||||
HeavyGravity, // pulled down harder: slower, can't jump, and falls faster
|
||||
Knockdown, // on the floor: can't do anything, and only attacks that hit the knocked down can hit it
|
||||
Executed // playing out how it was killed: can't do anything or be hit until it's over
|
||||
Executed, // playing out how it was killed: can't do anything or be hit until it's over
|
||||
Invulnerable // iframes: nothing at all can hit it, not even what hits the knocked down
|
||||
};
|
||||
|
||||
Type type = Type::HeavyGravity;
|
||||
@@ -23,13 +24,17 @@ struct StatusEffectInfo {
|
||||
bool preventsJumping = false;
|
||||
bool preventsActing = false; // can't move, turn, attack or anything else, like a stun
|
||||
bool dodgesHits = false; // only hitboxes that hit the knocked down can touch it
|
||||
bool invulnerable = false; // no hitbox can touch it
|
||||
};
|
||||
|
||||
inline const StatusEffectInfo& GetStatusEffectInfo(StatusEffect::Type type) {
|
||||
static const StatusEffectInfo heavyGravity = { "HEAVY", 0.5f, 3.0f, true };
|
||||
static const StatusEffectInfo knockdown = { "KNOCKED DOWN", 0.0f, 1.0f, true, true, true };
|
||||
static const StatusEffectInfo executed = { "", 0.0f, 1.0f, true, true, true };
|
||||
static const StatusEffectInfo executed = { "", 0.0f, 1.0f, true, true, true, true };
|
||||
static const StatusEffectInfo invulnerable = { "", 1.0f, 1.0f, false, false, false, true };
|
||||
switch (type) {
|
||||
case StatusEffect::Type::Invulnerable:
|
||||
return invulnerable;
|
||||
case StatusEffect::Type::Executed:
|
||||
return executed;
|
||||
case StatusEffect::Type::Knockdown:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user