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:
@@ -34,6 +34,10 @@ namespace Defaults::Physics {
|
|||||||
// Velocity heredada de la nau a l'explosió (80% del feel original).
|
// Velocity heredada de la nau a l'explosió (80% del feel original).
|
||||||
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
|
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
|
||||||
|
|
||||||
|
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
|
||||||
|
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
||||||
|
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
||||||
|
|
||||||
// Angular velocity sin for trajectory inheritance
|
// Angular velocity sin for trajectory inheritance
|
||||||
// Excess above this threshold is converted to tangential linear velocity
|
// Excess above this threshold is converted to tangential linear velocity
|
||||||
// Prevents "vortex trap" problem with high-rotation enemies
|
// Prevents "vortex trap" problem with high-rotation enemies
|
||||||
|
|||||||
@@ -255,10 +255,12 @@ void Enemy::destruir() {
|
|||||||
body_.radius = 0.0F; // No colisiona mientras está inactivo
|
body_.radius = 0.0F; // No colisiona mientras está inactivo
|
||||||
wounded_timer_ = 0.0F;
|
wounded_timer_ = 0.0F;
|
||||||
wound_expired_this_frame_ = false;
|
wound_expired_this_frame_ = false;
|
||||||
|
last_hit_by_ = 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Enemy::herir() {
|
void Enemy::herir(uint8_t shooter_id) {
|
||||||
wounded_timer_ = Defaults::Enemies::Wounded::DURATION;
|
wounded_timer_ = Defaults::Enemies::Wounded::DURATION;
|
||||||
|
last_hit_by_ = shooter_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Enemy::applyImpulse(const Vec2& impulse) {
|
void Enemy::applyImpulse(const Vec2& impulse) {
|
||||||
|
|||||||
@@ -86,11 +86,13 @@ class Enemy : public Entities::Entity {
|
|||||||
[[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; }
|
[[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; }
|
||||||
|
|
||||||
// Estado "herido": entre primer impacto de bala y explosión diferida.
|
// Estado "herido": entre primer impacto de bala y explosión diferida.
|
||||||
void herir();
|
// shooter_id: id del jugador que herí; 0xFF = sin atribución (cadena, etc.).
|
||||||
|
void herir(uint8_t shooter_id = 0xFF);
|
||||||
[[nodiscard]] auto isWounded() const -> bool { return wounded_timer_ > 0.0F; }
|
[[nodiscard]] auto isWounded() const -> bool { return wounded_timer_ > 0.0F; }
|
||||||
[[nodiscard]] auto getWoundedTimer() const -> float { return wounded_timer_; }
|
[[nodiscard]] auto getWoundedTimer() const -> float { return wounded_timer_; }
|
||||||
[[nodiscard]] auto woundExpiredThisFrame() const -> bool { return wound_expired_this_frame_; }
|
[[nodiscard]] auto woundExpiredThisFrame() const -> bool { return wound_expired_this_frame_; }
|
||||||
void consumeWoundExpired() { wound_expired_this_frame_ = false; }
|
void consumeWoundExpired() { wound_expired_this_frame_ = false; }
|
||||||
|
[[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; }
|
||||||
|
|
||||||
// Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico.
|
// Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico.
|
||||||
void applyImpulse(const Vec2& impulse);
|
void applyImpulse(const Vec2& impulse);
|
||||||
@@ -118,6 +120,7 @@ class Enemy : public Entities::Entity {
|
|||||||
// Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración.
|
// Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración.
|
||||||
float wounded_timer_{0.0F};
|
float wounded_timer_{0.0F};
|
||||||
bool wound_expired_this_frame_{false};
|
bool wound_expired_this_frame_{false};
|
||||||
|
uint8_t last_hit_by_{0xFF}; // 0xFF = sin atribución
|
||||||
|
|
||||||
// Métodos privados
|
// Métodos privados
|
||||||
void updateAnimation(float delta_time);
|
void updateAnimation(float delta_time);
|
||||||
|
|||||||
@@ -10,9 +10,75 @@
|
|||||||
|
|
||||||
namespace Systems::Collision {
|
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) {
|
void detectBulletEnemy(Context& ctx) {
|
||||||
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
|
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& bullet : ctx.bullets) {
|
||||||
for (auto& enemy : ctx.enemies) {
|
for (auto& enemy : ctx.enemies) {
|
||||||
@@ -21,11 +87,9 @@ namespace Systems::Collision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// *** COLISIÓN bullet → enemy ***
|
// *** COLISIÓN bullet → enemy ***
|
||||||
const Vec2& enemy_pos = enemy.getCenter();
|
|
||||||
|
|
||||||
// Empuje físico: impulse en la dirección bullet→enemy (fallback a la
|
// Empuje físico: impulse en la dirección bullet→enemy (fallback a la
|
||||||
// dirección de la bala si están exactamente solapados).
|
// 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) {
|
if (normal.lengthSquared() > 0.000001F) {
|
||||||
normal = normal.normalized();
|
normal = normal.normalized();
|
||||||
} else {
|
} else {
|
||||||
@@ -34,59 +98,33 @@ namespace Systems::Collision {
|
|||||||
}
|
}
|
||||||
enemy.applyImpulse(normal * Defaults::Physics::Bullet::IMPACT_IMPULSE);
|
enemy.applyImpulse(normal * Defaults::Physics::Bullet::IMPACT_IMPULSE);
|
||||||
|
|
||||||
// 1. Puntos según tipo
|
const uint8_t SHOOTER = bullet.getOwnerId();
|
||||||
int points = 0;
|
|
||||||
switch (enemy.getType()) {
|
if (enemy.isWounded()) {
|
||||||
case EnemyType::PENTAGON:
|
// Segundo impacto sobre enemy ya herido → muerte instantánea,
|
||||||
points = Defaults::Enemies::Scoring::PENTAGON_SCORE;
|
// puntos al nuevo shooter.
|
||||||
break;
|
explodeNow(ctx, enemy, SHOOTER);
|
||||||
case EnemyType::QUADRAT:
|
} else {
|
||||||
points = Defaults::Enemies::Scoring::QUADRAT_SCORE;
|
// Primer impacto → entra en estado herido (explosión diferida).
|
||||||
break;
|
enemy.herir(SHOOTER);
|
||||||
case EnemyType::MOLINILLO:
|
|
||||||
points = Defaults::Enemies::Scoring::MOLINILLO_SCORE;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
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) {
|
void detectShipEnemy(Context& ctx) {
|
||||||
constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
|
constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
|
||||||
|
|
||||||
@@ -158,6 +196,7 @@ namespace Systems::Collision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void detectAll(Context& ctx) {
|
void detectAll(Context& ctx) {
|
||||||
|
processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame
|
||||||
detectBulletEnemy(ctx);
|
detectBulletEnemy(ctx);
|
||||||
detectShipEnemy(ctx);
|
detectShipEnemy(ctx);
|
||||||
detectBulletPlayer(ctx);
|
detectBulletPlayer(ctx);
|
||||||
|
|||||||
@@ -43,12 +43,14 @@ struct Context {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Detecta colisiones bullet → enemy. Si hit:
|
// Detecta colisiones bullet → enemy. Si hit:
|
||||||
// - destruye el enemy (radius=0 en physics body)
|
// - Primer impacto: aplica impulse, marca al enemy como "herido", desactiva bullet.
|
||||||
// - crea debris + floating score
|
// - Segundo impacto (enemy ya herido): explosión inmediata + puntos al shooter.
|
||||||
// - desactiva la bullet
|
|
||||||
// - suma puntos al shooter
|
|
||||||
void detectBulletEnemy(Context& ctx);
|
void detectBulletEnemy(Context& ctx);
|
||||||
|
|
||||||
|
// Procesa enemigos cuyo wound timer ha expirado este frame: explosión + puntos
|
||||||
|
// al `last_hit_by_` del enemy (si está set).
|
||||||
|
void processWoundedDeaths(Context& ctx);
|
||||||
|
|
||||||
// Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id).
|
// Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id).
|
||||||
void detectShipEnemy(Context& ctx);
|
void detectShipEnemy(Context& ctx);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user