Files
orni-attack/source/game/systems/collision_system.cpp
T

260 lines
10 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"
namespace Systems::Collision {
namespace {
constexpr uint8_t NO_SHOOTER = 0xFF;
// Lookup tabla puntos / color por tipo de enemy (mantiene la lógica
// anterior pero centralizada para reutilizar entre paths).
auto scoreForType(EnemyType type) -> int {
switch (type) {
case EnemyType::PENTAGON:
return Defaults::Enemies::Scoring::PENTAGON_SCORE;
case EnemyType::QUADRAT:
return Defaults::Enemies::Scoring::QUADRAT_SCORE;
case EnemyType::MOLINILLO:
return Defaults::Enemies::Scoring::MOLINILLO_SCORE;
}
return 0;
}
auto colorForType(EnemyType type) -> SDL_Color {
switch (type) {
case EnemyType::PENTAGON:
return Defaults::Palette::PENTAGON;
case EnemyType::QUADRAT:
return Defaults::Palette::QUADRAT;
case EnemyType::MOLINILLO:
return Defaults::Palette::MOLINILLO;
}
return SDL_Color{};
}
// Mata al enemy con explosión: floating score, debris con velocity heredada,
// sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador.
// CRUCIAL: leer velocity/datos ANTES de destruir() (que zera la velocity).
void explodeNow(Context& ctx, Enemy& enemy, uint8_t shooter_id) {
const Vec2 ENEMY_POS = enemy.getCenter();
const Vec2 ENEMY_VEL = enemy.getVelocityVector();
const float BRIGHTNESS = enemy.getBrightness();
const auto SHAPE = enemy.getShape();
const EnemyType TYPE = enemy.getType();
const int POINTS = scoreForType(TYPE);
const SDL_Color COLOR = colorForType(TYPE);
if (shooter_id != NO_SHOOTER) {
ctx.score_per_player[shooter_id] += POINTS;
}
ctx.floating_score_manager.crear(POINTS, ENEMY_POS);
enemy.destruir();
constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suave)
const Vec2 INHERITED_VEL = ENEMY_VEL * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
ctx.debris_manager.explode(
SHAPE,
ENEMY_POS,
0.0F, // angle (rotación interna del enemy)
1.0F, // escala
VELOCITAT_EXPLOSIO,
BRIGHTNESS,
INHERITED_VEL,
0.0F, // sense herència angular: evita que els 5 trossos curvin en bloc
0.0F, // sin herencia visual
Defaults::Sound::EXPLOSION,
COLOR,
Defaults::Physics::Debris::ENEMY_LIFETIME,
Defaults::Physics::Debris::ENEMY_FRICTION,
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
// Firework burst radial des del centro de l'enemic (efecte adicional al debris).
// No heretem color: el burst usa el blanc per defecte per a un feel més lluminós.
ctx.firework_manager.spawn(ENEMY_POS);
}
} // anonymous namespace
void detectBulletEnemy(Context& ctx) {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
for (auto& bullet : ctx.bullets) {
if (!bullet.isActive()) {
continue;
}
for (auto& enemy : ctx.enemies) {
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, enemy, AMPLIFIER)) {
continue;
}
// *** COLISIÓN bullet → enemy ***
// Empuje físico cuasi-realista: el impulse és el moment de la bala
// (m·v) multiplicat pel factor de transferència. Direcció = vector
// velocity de la bala (cap a on viatjava).
const Vec2 IMPULSE = bullet.getBody().velocity *
(bullet.getBody().mass * Defaults::Physics::Bullet::IMPACT_MOMENTUM_FACTOR);
enemy.applyImpulse(IMPULSE);
const uint8_t SHOOTER = bullet.getOwnerId();
if (enemy.isWounded()) {
// Segundo impacto sobre enemy ya herido → muerte instantánea,
// puntos al nuevo shooter.
explodeNow(ctx, enemy, SHOOTER);
} else {
// Primer impacto → entra en estado herido (explosión diferida).
enemy.herir(SHOOTER);
}
bullet.desactivar();
break; // Una bala impacta a un enemy y muere
}
}
}
void processWoundedDeaths(Context& ctx) {
for (auto& enemy : ctx.enemies) {
if (!enemy.woundExpiredThisFrame()) {
continue;
}
enemy.consumeWoundExpired();
explodeNow(ctx, enemy, 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, 1.0F)) {
continue;
}
// El sano queda herido, propagando el shooter original.
if (A_WOUNDED) {
b.herir(a.getLastHitBy());
} else {
a.herir(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.
bool touching_now = false;
for (const auto& enemy : ctx.enemies) {
if (enemy.isInvulnerable()) {
continue;
}
if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) {
touching_now = true;
break;
}
}
// 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 definitiva (mateix flux que abans).
ctx.on_player_hit(i);
} else {
// Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld;
// l'enemic no rep dany per decisió de disseny).
ctx.ships[i].herir();
}
}
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.jugador1_actiu
: ctx.match_config.jugador2_actiu;
if (!JUGADOR_ACTIU) {
continue;
}
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, ctx.ships[player_id], AMPLIFIER)) {
continue;
}
// *** TEAMMATE HIT (friendly fire) ***
// Víctima perd 1 vida, atacant en guanya 1.
ctx.on_player_hit(player_id);
ctx.lives_per_player[BULLET_OWNER]++;
Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME);
bullet.desactivar();
break; // Una bullet solo impacta una vez por 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);
}
} // namespace Systems::Collision