295 lines
13 KiB
C++
295 lines
13 KiB
C++
// collision_system.cpp - Implementación del sistema de colisiones
|
|
|
|
#include "game/systems/collision_system.hpp"
|
|
|
|
#include <cstdint>
|
|
|
|
#include "core/audio/audio.hpp"
|
|
#include "core/physics/collision.hpp"
|
|
#include "core/types.hpp"
|
|
#include "game/constants.hpp"
|
|
#include "game/entities/bullet_config.hpp"
|
|
#include "game/systems/enemy_event_dispatcher.hpp"
|
|
|
|
namespace Systems::Collision {
|
|
|
|
namespace {
|
|
|
|
// Trenca una bala en debris (8 fragments de l'octàgon) + so HIT + desactiva.
|
|
// S'invoca des de qualsevol desactivació de bala (impacte amb enemic, amb jugador,
|
|
// o sortida del PLAYAREA) per a un feedback visual i sonor consistent.
|
|
void breakBullet(Effects::DebrisManager& debris_manager, Bullet& bullet) {
|
|
constexpr float DEBRIS_VELOCITY = 60.0F;
|
|
debris_manager.explode(
|
|
bullet.getShape(),
|
|
bullet.getCenter(),
|
|
bullet.getAngle(),
|
|
1.0F, // scale
|
|
DEBRIS_VELOCITY,
|
|
bullet.getBrightness(),
|
|
Vec2{}, // sense herència de velocitat (fragments radials)
|
|
0.0F, // sense velocity angular heretada
|
|
0.0F, // sense rotació visual heretada
|
|
Defaults::Sound::HIT,
|
|
bullet.getConfig().colors.normal,
|
|
Defaults::Physics::Debris::TEMPS_VIDA,
|
|
Defaults::Physics::Debris::ACCELERACIO,
|
|
1); // sense duplicat de segments
|
|
bullet.desactivar();
|
|
}
|
|
} // anonymous namespace
|
|
|
|
void detectBulletEnemy(Context& ctx) {
|
|
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
|
|
constexpr uint8_t NO_SHOOTER = 0xFF;
|
|
constexpr uint8_t ENEMY_BASE = Defaults::Entities::ENEMY_OWNER_BASE;
|
|
|
|
for (auto& bullet : ctx.bullets) {
|
|
if (!bullet.isActive()) {
|
|
continue;
|
|
}
|
|
const uint8_t BULLET_OWNER = bullet.getOwnerId();
|
|
const bool IS_ENEMY_BULLET = BULLET_OWNER >= ENEMY_BASE;
|
|
|
|
for (std::size_t i = 0; i < ctx.enemies.size(); ++i) {
|
|
Enemy& enemy = ctx.enemies[i];
|
|
// Self-shot: una bala d'enemic mai no impacta el seu propi creador.
|
|
if (IS_ENEMY_BULLET && BULLET_OWNER == static_cast<uint8_t>(ENEMY_BASE + i)) {
|
|
continue;
|
|
}
|
|
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), enemy, AMPLIFIER)) {
|
|
continue;
|
|
}
|
|
|
|
// *** COLISIÓN bullet → enemy ***
|
|
// La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu
|
|
// al YAML de l'enemic via la secció `events:`. Si la bala és d'enemic,
|
|
// no atribuïm el kill a cap player (NO_SHOOTER).
|
|
const uint8_t SHOOTER = IS_ENEMY_BULLET ? NO_SHOOTER : BULLET_OWNER;
|
|
Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, SHOOTER, &bullet);
|
|
breakBullet(ctx.debris_manager, bullet);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void processWoundedDeaths(Context& ctx) {
|
|
for (auto& enemy : ctx.enemies) {
|
|
if (!enemy.woundExpiredThisFrame()) {
|
|
continue;
|
|
}
|
|
enemy.consumeWoundExpired();
|
|
Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HURT_END, enemy.getLastHitBy());
|
|
}
|
|
}
|
|
|
|
void detectWoundedChain(Context& ctx) {
|
|
const std::size_t N = ctx.enemies.size();
|
|
for (std::size_t i = 0; i < N; i++) {
|
|
Enemy& a = ctx.enemies[i];
|
|
if (!a.isCollidable()) {
|
|
continue;
|
|
}
|
|
for (std::size_t j = i + 1; j < N; j++) {
|
|
Enemy& b = ctx.enemies[j];
|
|
if (!b.isCollidable()) {
|
|
continue;
|
|
}
|
|
const bool A_WOUNDED = a.isWounded();
|
|
const bool B_WOUNDED = b.isWounded();
|
|
if (A_WOUNDED == B_WOUNDED) {
|
|
continue; // ambos sanos o ambos heridos: nada que propagar
|
|
}
|
|
if (!Physics::checkCollision(a, b, Defaults::Game::COLLISION_WOUNDED_CHAIN_AMPLIFIER)) {
|
|
continue;
|
|
}
|
|
// El sano queda herido, propagando el shooter original.
|
|
if (A_WOUNDED) {
|
|
b.hurt(a.getLastHitBy());
|
|
} else {
|
|
a.hurt(b.getLastHitBy());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void detectShipEnemy(Context& ctx) {
|
|
constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
|
|
|
|
for (uint8_t i = 0; i < 2; i++) {
|
|
// Skip si ya tocado / muerto / invulnerable. NO actualitzem el flag de contacte:
|
|
// mentre estem inactius no hi ha "frame anterior" rellevant, i el respawn ja el resetea.
|
|
if (ctx.hit_timer_per_player[i] > 0.0F ||
|
|
!ctx.ships[i].isActive() ||
|
|
ctx.ships[i].isInvulnerable()) {
|
|
continue;
|
|
}
|
|
|
|
// Comprovem si la nau toca QUALSEVOL enemic vulnerable aquest frame.
|
|
Enemy* touched_enemy = nullptr;
|
|
for (auto& enemy : ctx.enemies) {
|
|
if (enemy.isInvulnerable()) {
|
|
continue;
|
|
}
|
|
if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) {
|
|
touched_enemy = &enemy;
|
|
break;
|
|
}
|
|
}
|
|
const bool TOUCHING_NOW = touched_enemy != nullptr;
|
|
|
|
// Edge-trigger: només compta com a impacte la transició no-tocant → tocant.
|
|
// Així el contacte continu durant el rebot frame-a-frame no dispara HURT i mort
|
|
// en frames consecutius.
|
|
const bool RISING_EDGE = TOUCHING_NOW && !ctx.ships[i].wasTouchingEnemyPrevFrame();
|
|
if (RISING_EDGE) {
|
|
if (ctx.ships[i].isHurt()) {
|
|
// Segon impacte durant HURT → mort. Aplica un impuls afegit
|
|
// perquè l'enemic surti disparat (feedback visible).
|
|
const Vec2 SHIP_VEL = ctx.ships[i].getVelocityVector();
|
|
const float DEATH_FACTOR = ctx.ships[i].getConfig().physics.death_impact_factor;
|
|
const Vec2 IMPULSE = SHIP_VEL * (ctx.ships[i].getBody().mass * DEATH_FACTOR);
|
|
touched_enemy->applyImpulse(IMPULSE);
|
|
// Sense bala: cap impuls de bala per als debris (mort per
|
|
// col·lisió cos-cos). Els debris hereten la inèrcia del ship.
|
|
ctx.on_player_hit(i, Vec2{});
|
|
} else {
|
|
// Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld;
|
|
// l'enemic no rep dany per decisió de disseny).
|
|
ctx.ships[i].hurt();
|
|
}
|
|
}
|
|
ctx.ships[i].setTouchingEnemyPrevFrame(TOUCHING_NOW);
|
|
}
|
|
}
|
|
|
|
void detectBulletPlayer(Context& ctx) {
|
|
if (!Defaults::Game::FRIENDLY_FIRE_ENABLED) {
|
|
return;
|
|
}
|
|
|
|
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER;
|
|
|
|
for (auto& bullet : ctx.bullets) {
|
|
if (!bullet.isActive()) {
|
|
continue;
|
|
}
|
|
const uint8_t BULLET_OWNER = bullet.getOwnerId();
|
|
|
|
for (uint8_t player_id = 0; player_id < 2; player_id++) {
|
|
// Una bala mai no impacta al seu propi shooter: les bales d'aquest joc no
|
|
// reboten ni el shooter pot atrapar-les, així que la prevenció és per disseny.
|
|
if (BULLET_OWNER == player_id) {
|
|
continue;
|
|
}
|
|
if (ctx.hit_timer_per_player[player_id] > 0.0F ||
|
|
!ctx.ships[player_id].isActive() ||
|
|
ctx.ships[player_id].isInvulnerable()) {
|
|
continue;
|
|
}
|
|
const bool JUGADOR_ACTIU = (player_id == 0)
|
|
? ctx.match_config.player1_active
|
|
: ctx.match_config.player2_active;
|
|
if (!JUGADOR_ACTIU) {
|
|
continue;
|
|
}
|
|
|
|
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), ctx.ships[player_id], AMPLIFIER)) {
|
|
continue;
|
|
}
|
|
|
|
// *** TEAMMATE HIT (friendly fire) ***
|
|
// Víctima perd 1 vida, atacant en guanya 1. Friendly fire sempre
|
|
// mata: el bullet va als debris (via tocado) i NO al cos del ship
|
|
// — el cos està a punt de desactivar-se, qualsevol impuls seria
|
|
// double-count amb la velocitat que ja reben els trossos.
|
|
const Vec2 BULLET_VEL = bullet.getBody().velocity;
|
|
ctx.on_player_hit(player_id, BULLET_VEL);
|
|
ctx.lives_per_player[BULLET_OWNER]++;
|
|
Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME);
|
|
breakBullet(ctx.debris_manager, bullet);
|
|
break; // Una bullet solo impacta una vez por frame
|
|
}
|
|
}
|
|
}
|
|
|
|
void detectEnemyBulletShip(Context& ctx) {
|
|
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER;
|
|
constexpr uint8_t ENEMY_BASE = Defaults::Entities::ENEMY_OWNER_BASE;
|
|
|
|
for (auto& bullet : ctx.bullets) {
|
|
if (!bullet.isActive() || bullet.getOwnerId() < ENEMY_BASE) {
|
|
continue;
|
|
}
|
|
for (uint8_t player_id = 0; player_id < 2; player_id++) {
|
|
if (ctx.hit_timer_per_player[player_id] > 0.0F ||
|
|
!ctx.ships[player_id].isActive() ||
|
|
ctx.ships[player_id].isInvulnerable()) {
|
|
continue;
|
|
}
|
|
const bool JUGADOR_ACTIU = (player_id == 0)
|
|
? ctx.match_config.player1_active
|
|
: ctx.match_config.player2_active;
|
|
if (!JUGADOR_ACTIU) {
|
|
continue;
|
|
}
|
|
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), ctx.ships[player_id], AMPLIFIER)) {
|
|
continue;
|
|
}
|
|
|
|
// *** BALA D'ENEMIC → SHIP ***
|
|
// Regla "cos XOR trossos": l'impuls de la bala s'aplica al cos
|
|
// només si el ship sobreviu (fereix). Si el ship mor, el bullet
|
|
// va directament als trossos (via tocado) i el cos no rep impuls
|
|
// — els trossos ja porten la força de la bala, qualsevol impuls
|
|
// afegit al cos seria double-count.
|
|
const Vec2 BULLET_VEL = bullet.getBody().velocity;
|
|
if (ctx.ships[player_id].isHurt()) {
|
|
// Segon impacte durant HURT → mort.
|
|
ctx.on_player_hit(player_id, BULLET_VEL);
|
|
} else {
|
|
// Fereix: el cos sobreviu, rep l'impuls. No hi ha debris encara.
|
|
const Vec2 IMPULSE = BULLET_VEL *
|
|
(bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor);
|
|
ctx.ships[player_id].getBody().applyImpulse(IMPULSE);
|
|
ctx.ships[player_id].hurt();
|
|
}
|
|
breakBullet(ctx.debris_manager, bullet);
|
|
break; // una bala impacta una vegada per frame
|
|
}
|
|
}
|
|
}
|
|
|
|
void detectAll(Context& ctx) {
|
|
processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame
|
|
detectBulletEnemy(ctx);
|
|
detectWoundedChain(ctx); // un herit pot ferir a un sa al fregar-lo
|
|
detectShipEnemy(ctx);
|
|
detectBulletPlayer(ctx);
|
|
detectEnemyBulletShip(ctx);
|
|
}
|
|
|
|
void desactivateOutOfBoundsBullets(
|
|
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets,
|
|
Effects::DebrisManager& debris_manager) {
|
|
float min_x;
|
|
float max_x;
|
|
float min_y;
|
|
float max_y;
|
|
Constants::getPlayAreaBounds(min_x, max_x, min_y, max_y);
|
|
|
|
for (auto& bullet : bullets) {
|
|
if (!bullet.isActive()) {
|
|
continue;
|
|
}
|
|
const float R = bullet.getCollisionRadius();
|
|
const Vec2& pos = bullet.getCenter();
|
|
if (pos.x < min_x + R || pos.x > max_x - R ||
|
|
pos.y < min_y + R || pos.y > max_y - R) {
|
|
breakBullet(debris_manager, bullet);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace Systems::Collision
|