finished the game (i will probably not be touching this again)

This commit is contained in:
2026-11-08 12:51:19 -05:00
parent 36e093f84c
commit 5d7aa3b93b
37 changed files with 141 additions and 11 deletions
+11
View File
@@ -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)
+2
View File
@@ -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.
+8
View File
@@ -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;
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.