feat(collision): primer impacte fereix, segon mata; mort diferida via timer (Fase 3)

- Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE (placeholder 1.0).
- Enemy::herir(shooter_id) emmagatzema last_hit_by_ per a atribució posterior.
- collision_system: helper anònim explodeNow(ctx, enemy, shooter_id) que
  llegeix velocity/dades ABANS de destruir() (corregeix bug latent: el codi
  anterior llegia getVelocityVector() després de destruir, que zera velocity
  → l'explosió mai heretava inèrcia).
- detectBulletEnemy: primer impacte aplica impulse + herir(); segon impacte
  sobre enemy ferit dispara explodeNow immediata.
- processWoundedDeaths: explota enemics amb wound timer expirat aquest frame.
- detectAll: processWoundedDeaths abans de detectBulletEnemy (les expiracions
  maten primer; les bales del mateix frame ja no toquen el cos destruït).

Puntos s'atribueixen a la mort real, no a l'impacte inicial.

Build neta i smoke test xvfb OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 10:26:13 +02:00
parent dc2824a095
commit 5cb547db0a
5 changed files with 120 additions and 70 deletions
+89 -50
View File
@@ -10,9 +10,75 @@
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 DROTACIO = enemy.getRotationDelta();
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,
DROTACIO,
0.0F, // sin herencia visual
Defaults::Sound::EXPLOSION,
COLOR);
}
} // anonymous namespace
void detectBulletEnemy(Context& ctx) {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suau)
for (auto& bullet : ctx.bullets) {
for (auto& enemy : ctx.enemies) {
@@ -21,11 +87,9 @@ namespace Systems::Collision {
}
// *** COLISIÓN bullet → enemy ***
const Vec2& enemy_pos = enemy.getCenter();
// Empuje físico: impulse en la dirección bullet→enemy (fallback a la
// dirección de la bala si están exactamente solapados).
Vec2 normal = enemy_pos - bullet.getCenter();
Vec2 normal = enemy.getCenter() - bullet.getCenter();
if (normal.lengthSquared() > 0.000001F) {
normal = normal.normalized();
} else {
@@ -34,59 +98,33 @@ namespace Systems::Collision {
}
enemy.applyImpulse(normal * Defaults::Physics::Bullet::IMPACT_IMPULSE);
// 1. Puntos según tipo
int points = 0;
switch (enemy.getType()) {
case EnemyType::PENTAGON:
points = Defaults::Enemies::Scoring::PENTAGON_SCORE;
break;
case EnemyType::QUADRAT:
points = Defaults::Enemies::Scoring::QUADRAT_SCORE;
break;
case EnemyType::MOLINILLO:
points = Defaults::Enemies::Scoring::MOLINILLO_SCORE;
break;
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);
}
uint8_t owner_id = bullet.getOwnerId();
ctx.score_per_player[owner_id] += points;
ctx.floating_score_manager.crear(points, enemy_pos);
// 2. Destruir enemy + crear explosión (debris hereda color del enemy)
SDL_Color enemy_color{};
switch (enemy.getType()) {
case EnemyType::PENTAGON:
enemy_color = Defaults::Palette::PENTAGON;
break;
case EnemyType::QUADRAT:
enemy_color = Defaults::Palette::QUADRAT;
break;
case EnemyType::MOLINILLO:
enemy_color = Defaults::Palette::MOLINILLO;
break;
}
enemy.destruir();
Vec2 vel_enemic = enemy.getVelocityVector();
ctx.debris_manager.explode(
enemy.getShape(),
enemy_pos,
0.0F, // angle (la rotación es interna del enemy)
1.0F, // escala
VELOCITAT_EXPLOSIO,
enemy.getBrightness(),
vel_enemic,
enemy.getRotationDelta(),
0.0F, // sin herencia visual
Defaults::Sound::EXPLOSION,
enemy_color);
// 3. Desactivar bullet (solo destruye 1 enemy)
bullet.desactivar();
break;
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 detectShipEnemy(Context& ctx) {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
@@ -158,6 +196,7 @@ namespace Systems::Collision {
}
void detectAll(Context& ctx) {
processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame
detectBulletEnemy(ctx);
detectShipEnemy(ctx);
detectBulletPlayer(ctx);