Compare commits

...

6 Commits

26 changed files with 434 additions and 117 deletions
@@ -0,0 +1,82 @@
name: big_pentagon
ai_type: big_pentagon # Validat contra el directori; mapeja a EnemyType::BIG_PENTAGON.
# Shape circular pròpia (anell exterior + anell interior + 6 radis + nucli),
# pensada per llegir-se com a "reactor / orb" amb més detall que els enemics
# petits.
shape:
path: enemy_big_orb.shp
scale: 1.5
collision_factor: 1.0
physics:
mass: 50.0 # Molt pesat: una bala el frena un poc però no el "envia a passejar".
speed: 50.0 # Avança decidit cap al ship (no és lent passiu, és amenaça constant).
rotation_delta_min: 0.3
rotation_delta_max: 1.5
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
ai:
# Persecució contínua del ship més proper. chase_strength alt (1.0 = ~1s
# per realinear-se) perquè, encara que una bala l'empentja lateralment,
# ràpidament torna a posar la seua proa cap al jugador.
movement:
type: chase
chase_strength: 1.0
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.5 # Una mica més llarg que els altres (és un boss).
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 54.0 # 1.5× del normal (alineat amb scale 1.5).
colors:
normal: [66, 195, 208] # #42C3D0 — turquesa-cyan distintiu per al boss.
wounded: [255, 220, 60]
score: 500 # 5x un enemic normal: aguanta 10x més.
# Estrenant el sistema HP: 10 unitats. Cada bala fa decrease_health + flash
# + create_debris_partial (xip a 0.3x) + create_fireworks_small (espurna).
# Al 10è hit (HP=0), on_no_health encadena destroy directe — sense passar
# per wounded (com Star). 10 HP ja és prou dificultat sense afegir un hit
# extra.
health: 10
events:
on_hit:
- action: decrease_health # primer: si arriba a 0 dispara on_no_health
#- action: flash # feedback visual de damage parcial
- action: create_debris_partial # xip a 0.3x mida (sense ser letal)
- action: create_fireworks_small # espurna a cada hit (12 punts, lent)
- action: apply_impulse # empenta el cos (skip si will_die)
on_no_health:
- action: destroy # mort directa, sense wounded
on_destroy:
- action: add_score
- action: create_debris # explosió completa
- action: create_fireworks
+6 -2
View File
@@ -55,10 +55,14 @@ colors:
score: 100 score: 100
events: events:
# Comportament clàssic: dos impactes per matar (set_hurt entra wounded; # HP=1 (default): decrement → on_no_health → set_hurt wounded → mort.
# el segon hit detecta wounded i destrueix automàticament). # decrease_health primer perquè si la mort cau aquí (segon hit durant wounded),
# el dispatcher salta la resta del chain (incloent apply_impulse) sobre el
# cos ja destruït.
on_hit: on_hit:
- action: decrease_health
- action: apply_impulse - action: apply_impulse
on_no_health:
- action: set_hurt - action: set_hurt
on_hurt_end: on_hurt_end:
- action: destroy - action: destroy
+3
View File
@@ -55,8 +55,11 @@ colors:
score: 200 score: 200
events: events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
on_hit: on_hit:
- action: decrease_health
- action: apply_impulse - action: apply_impulse
on_no_health:
- action: set_hurt - action: set_hurt
on_hurt_end: on_hurt_end:
- action: destroy - action: destroy
+3
View File
@@ -56,8 +56,11 @@ colors:
score: 150 score: 150
events: events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
on_hit: on_hit:
- action: decrease_health
- action: apply_impulse - action: apply_impulse
on_no_health:
- action: set_hurt - action: set_hurt
on_hurt_end: on_hurt_end:
- action: destroy - action: destroy
+3 -1
View File
@@ -66,8 +66,10 @@ score: 100
events: events:
# STAR: mor al primer impacte, sense passar per wounded. # STAR: mor al primer impacte, sense passar per wounded.
# HP=1 (default): decrement → on_no_health → destroy directe (sense wounded).
on_hit: on_hit:
- action: apply_impulse - action: decrease_health
on_no_health:
- action: destroy - action: destroy
on_destroy: on_destroy:
- action: add_score - action: add_score
+32
View File
@@ -0,0 +1,32 @@
# enemy_big_orb.shp - ORNI enemic gegant (orb circular, doble anell amb radis)
# © 2026 JailDesigner
#
# Forma "reactor / boss circular" — més detall que els enemics petits perquè
# es renderitza a escala 1.5x i ha de llegir-se com a amenaça gran.
# - Anell exterior: dodecàgon (12 vèrtexs) — aparença circular suau, radi 20.
# - Anell interior: hexàgon (6 vèrtexs, rotat 30°) — radi 10.
# - 6 radis curts que connecten l'anell interior amb l'exterior.
# - Petit "+" central com a nucli.
# Bounding radius natiu = 20 (alineat amb la resta d'enemics).
name: enemy_big_orb
scale: 1.0
center: 0, 0
# Anell exterior (dodecàgon, vèrtex apuntant amunt)
polyline: 0,-20 10,-17.32 17.32,-10 20,0 17.32,10 10,17.32 0,20 -10,17.32 -17.32,10 -20,0 -17.32,-10 -10,-17.32 0,-20
# Anell interior (hexàgon, vèrtex apuntant a la dreta — rotat 30° respecte l'exterior)
polyline: 5,-8.66 10,0 5,8.66 -5,8.66 -10,0 -5,-8.66 5,-8.66
# 6 radis: del vèrtex de l'hexàgon interior al vèrtex corresponent del dodecàgon exterior
line: 5,-8.66 10,-17.32
line: 10,0 20,0
line: 5,8.66 10,17.32
line: -5,8.66 -10,17.32
line: -10,0 -20,0
line: -5,-8.66 -10,-17.32
# Nucli central: petit "+" (2 segments creuats, radi 3)
line: -3,0 3,0
line: 0,-3 0,3
+3 -3
View File
@@ -7,7 +7,7 @@ metadata:
description: "Progressive difficulty curve from novice to expert" description: "Progressive difficulty curve from novice to expert"
stages: stages:
# STAGE 1: Tutorial - Mix de tots 4 tipus al 25% per mostrar-los junts # STAGE 1: Tutorial - 4 tipus (sense star: les bales fan injugable el test).
- stage_id: 1 - stage_id: 1
total_enemies: 50 total_enemies: 50
spawn_config: spawn_config:
@@ -15,10 +15,10 @@ stages:
initial_delay: 0.3 initial_delay: 0.3
spawn_interval: 0.4 spawn_interval: 0.4
enemy_distribution: enemy_distribution:
pentagon: 25 pentagon: 30
cuadrado: 25 cuadrado: 25
molinillo: 25 molinillo: 25
star: 25 big_pentagon: 20
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 0.7 speed_multiplier: 0.7
rotation_multiplier: 0.8 rotation_multiplier: 0.8
+27
View File
@@ -16,3 +16,30 @@ namespace Defaults::Enemies::Spawn {
constexpr int MAX_SPAWN_ATTEMPTS = 50; constexpr int MAX_SPAWN_ATTEMPTS = 50;
} // namespace Defaults::Enemies::Spawn } // namespace Defaults::Enemies::Spawn
namespace Defaults::Enemies::Visual {
// Duració del "flash" que dispara l'acció FLASH (feedback per impacte
// parcial en enemics HP>1). Curt: l'efecte ha de llegir-se com un cop,
// no com una transició.
constexpr float FLASH_DURATION = 0.08F;
} // namespace Defaults::Enemies::Visual
namespace Defaults::Enemies::Debris {
// Escala dels fragments per a l'acció CREATE_DEBRIS_PARTIAL (xip d'impacte
// en enemics HP>1). 0.3 = trossos petits, com de "casc esquerdat".
constexpr float PARTIAL_PIECE_SCALE = 0.3F;
} // namespace Defaults::Enemies::Debris
namespace Defaults::Enemies::Fireworks {
// Paràmetres del firework "petit" per a l'acció CREATE_FIREWORKS_SMALL
// (feedback per impacte parcial en enemics HP>1). Pocs punts i baixa
// velocitat: una espurna breu, no una explosió.
constexpr int SMALL_N_POINTS = 20;
constexpr float SMALL_SPEED = 250.0F;
} // namespace Defaults::Enemies::Fireworks
+7
View File
@@ -43,6 +43,13 @@ namespace Defaults::Physics::Debris {
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua. // 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F; constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
// Velocitat de la bala traspassada a cada fragment de debris al moment
// de l'impacte. Separat de la inèrcia del cos (velocitat_objecte): permet
// que els trossos volin "amb la força de la bala" encara que el cos pesi
// molt i amb prou feines es mogui. 0.4 a 700 px/s = ~280 px/s extra per
// fragment, molt visible sense ser excessiu.
constexpr float BULLET_IMPULSE_FACTOR = 0.4F;
// Tuneig específic de l'explosió d'enemic (overrides als defaults // Tuneig específic de l'explosió d'enemic (overrides als defaults
// que es passen com a paràmetres opcionals a explode()). // que es passen com a paràmetres opcionals a explode()).
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
+28 -13
View File
@@ -57,7 +57,9 @@ namespace Effects {
SDL_Color color, SDL_Color color,
float lifetime, float lifetime,
float friction, float friction,
int segment_multiplier) { int segment_multiplier,
const Vec2& bullet_impulse_velocity,
float piece_scale) {
if (!shape || !shape->isValid()) { if (!shape || !shape->isValid()) {
return; return;
} }
@@ -84,7 +86,7 @@ namespace Effects {
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale); Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
// Si el pool es ple, no té sentit continuar amb la resta de segments // Si el pool es ple, no té sentit continuar amb la resta de segments
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction)) { if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction, bullet_impulse_velocity, piece_scale)) {
return; return;
} }
} }
@@ -110,34 +112,47 @@ namespace Effects {
return segments; return segments;
} }
auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool { auto DebrisManager::spawnDebris(const Vec2& world_p1_in, const Vec2& world_p2_in, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool {
Debris* debris = findFreeSlot(); Debris* debris = findFreeSlot();
if (debris == nullptr) { if (debris == nullptr) {
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n"; std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
return false; return false;
} }
// Escala el segment al voltant del seu punt mitjà segons piece_scale
// (1.0 = original; 0.3 = "esquerda petita"). La resta del càlcul (angle,
// half_length, p1/p2) en deriva naturalment.
const Vec2 MID = {.x = (world_p1_in.x + world_p2_in.x) / 2.0F,
.y = (world_p1_in.y + world_p2_in.y) / 2.0F};
const Vec2 WORLD_P1 = {.x = MID.x + ((world_p1_in.x - MID.x) * piece_scale),
.y = MID.y + ((world_p1_in.y - MID.y) * piece_scale)};
const Vec2 WORLD_P2 = {.x = MID.x + ((world_p2_in.x - MID.x) * piece_scale),
.y = MID.y + ((world_p2_in.y - MID.y) * piece_scale)};
// Geometria autoritaritzada: centro + original_angle + original_half_length. // Geometria autoritaritzada: centro + original_angle + original_half_length.
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades. // p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades.
const float DX = world_p2.x - world_p1.x; const float DX = WORLD_P2.x - WORLD_P1.x;
const float DY = world_p2.y - world_p1.y; const float DY = WORLD_P2.y - WORLD_P1.y;
debris->centro = {.x = (world_p1.x + world_p2.x) / 2.0F, debris->centro = {.x = (WORLD_P1.x + WORLD_P2.x) / 2.0F,
.y = (world_p1.y + world_p2.y) / 2.0F}; .y = (WORLD_P1.y + WORLD_P2.y) / 2.0F};
debris->original_angle = std::atan2(DY, DX); debris->original_angle = std::atan2(DY, DX);
debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F; debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F;
debris->p1 = world_p1; debris->p1 = WORLD_P1;
debris->p2 = world_p2; debris->p2 = WORLD_P2;
// Direcció radial (desde el centro hacia el segment) // Direcció radial (desde el centro hacia el segment)
Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro); Vec2 direccio = computeExplosionDirection(WORLD_P1, WORLD_P2, centro);
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte) // Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte +
// velocitat de la bala escalada per BULLET_IMPULSE_FACTOR).
float speed = float speed =
velocitat_base + velocitat_base +
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) * (((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
Defaults::Physics::Debris::VARIACIO_SPEED); Defaults::Physics::Debris::VARIACIO_SPEED);
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x; debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x +
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y; (bullet_impulse_velocity.x * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR);
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y +
(bullet_impulse_velocity.y * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR);
debris->acceleration = friction; debris->acceleration = friction;
// Rotación de trayectoria (con conversió a tangencial si excedeix cap) // Rotación de trayectoria (con conversió a tangencial si excedeix cap)
+12 -2
View File
@@ -47,6 +47,14 @@ namespace Effects {
// - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s) // - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s)
// - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60) // - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60)
// - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar) // - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar)
// - bullet_impulse_velocity: velocitat de la bala que ha causat l'impacte (px/s,
// per defecte 0). S'aplica a cada fragment escalada per
// Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR, independent de
// velocitat_objecte. Permet que els trossos "salten amb la força de la bala"
// encara que el cos sigui pesat i amb prou feines es mogui.
// - piece_scale: multiplicador de la longitud de cada fragment al spawn
// (per defecte 1.0). Útil per a debris "parcial" d'impactes no letals
// en enemics HP>1 (trossos petits, com d'esquerda).
void explode(const std::shared_ptr<Graphics::Shape>& shape, void explode(const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& centro, const Vec2& centro,
float angle, float angle,
@@ -60,7 +68,9 @@ namespace Effects {
SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global
float lifetime = Defaults::Physics::Debris::TEMPS_VIDA, float lifetime = Defaults::Physics::Debris::TEMPS_VIDA,
float friction = Defaults::Physics::Debris::ACCELERACIO, float friction = Defaults::Physics::Debris::ACCELERACIO,
int segment_multiplier = 1); int segment_multiplier = 1,
const Vec2& bullet_impulse_velocity = {.x = 0.0F, .y = 0.0F},
float piece_scale = 1.0F);
// Actualitzar todos los fragments active // Actualitzar todos los fragments active
void update(float delta_time); void update(float delta_time);
@@ -97,7 +107,7 @@ namespace Effects {
-> std::vector<std::pair<Vec2, Vec2>>; -> std::vector<std::pair<Vec2, Vec2>>;
// Inicialitza un debris en un slot lliure i el deixa actiu. Retorna // Inicialitza un debris en un slot lliure i el deixa actiu. Retorna
// false si el pool está ple (la cridadora ha d'aturar el bucle). // false si el pool está ple (la cridadora ha d'aturar el bucle).
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool; auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool;
static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular); static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular);
static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual); static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual);
}; };
+18 -1
View File
@@ -133,6 +133,9 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
invulnerability_timer_ = cfg.spawn.invulnerability_duration; invulnerability_timer_ = cfg.spawn.invulnerability_duration;
brightness_ = cfg.spawn.invulnerability_brightness_start; brightness_ = cfg.spawn.invulnerability_brightness_start;
health_ = cfg.health;
flash_timer_ = 0.0F;
is_active_ = true; is_active_ = true;
} }
@@ -150,6 +153,11 @@ void Enemy::update(float delta_time) {
} }
} }
if (flash_timer_ > 0.0F) {
flash_timer_ -= delta_time;
flash_timer_ = std::max(flash_timer_, 0.0F);
}
if (invulnerability_timer_ > 0.0F) { if (invulnerability_timer_ > 0.0F) {
invulnerability_timer_ -= delta_time; invulnerability_timer_ -= delta_time;
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F); invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
@@ -192,7 +200,15 @@ void Enemy::draw() const {
} }
} }
Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, brightness_, color); // Flash d'impacte parcial (HP>1): força el color a blanc i el brillo a
// 1.0 durant la finestra de flash. Té prioritat sobre el blink wounded.
float effective_brightness = brightness_;
if (flash_timer_ > 0.0F) {
color = SDL_Color{.r = 255, .g = 255, .b = 255, .a = 255};
effective_brightness = 1.0F;
}
Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, effective_brightness, color);
} }
void Enemy::destroy() { void Enemy::destroy() {
@@ -203,6 +219,7 @@ void Enemy::destroy() {
wounded_timer_ = 0.0F; wounded_timer_ = 0.0F;
wound_expired_this_frame_ = false; wound_expired_this_frame_ = false;
last_hit_by_ = 0xFF; last_hit_by_ = 0xFF;
flash_timer_ = 0.0F;
} }
void Enemy::hurt(uint8_t shooter_id) { void Enemy::hurt(uint8_t shooter_id) {
+27 -1
View File
@@ -8,6 +8,7 @@
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
#include "core/defaults/enemies.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/entities/enemy_ai.hpp" #include "game/entities/enemy_ai.hpp"
@@ -19,7 +20,8 @@ enum class EnemyType : uint8_t {
PENTAGON = 0, // Pentágono esquivador (zigzag) PENTAGON = 0, // Pentágono esquivador (zigzag)
SQUARE = 1, // Square perseguidor (tracks ship) SQUARE = 1, // Square perseguidor (tracks ship)
PINWHEEL = 2, // Molinillo agresivo (rápido, girando) PINWHEEL = 2, // Molinillo agresivo (rápido, girando)
STAR = 3 // Estrella de 5 puntes (clone visual de Pentagon, comportament zigzag) STAR = 3, // Estrella de 5 puntes (clone visual de Pentagon, comportament zigzag)
BIG_PENTAGON = 4, // Pentàgon gegant tough (HP=10, chase lent — primer enemic HP>1)
}; };
// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp. // Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp.
@@ -116,6 +118,20 @@ class Enemy : public Entities::Entity {
void consumeWoundExpired() { wound_expired_this_frame_ = false; } void consumeWoundExpired() { wound_expired_this_frame_ = false; }
[[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; } [[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; }
// Salut: decrementada per l'acció DECREASE_HEALTH al dispatcher d'events.
// Quan arriba a 0 o menys, el dispatcher dispara ON_NO_HEALTH (que
// típicament encadena SET_HURT o DESTROY al YAML). last_hit_by s'actualitza
// al decrement perquè la mort posterior atribueixi correctament el kill.
[[nodiscard]] auto getHealth() const -> int { return health_; }
void decrementHealth(uint8_t shooter_id = 0xFF) {
--health_;
last_hit_by_ = shooter_id;
}
// Flash visual: brief impacto-feedback quan rep un hit no letal (HP>1).
// Disparat per l'acció FLASH; el render alça la lluminositat mentre dura.
void triggerFlash() { flash_timer_ = Defaults::Enemies::Visual::FLASH_DURATION; }
// 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);
@@ -154,6 +170,16 @@ class Enemy : public Entities::Entity {
bool wound_expired_this_frame_{false}; bool wound_expired_this_frame_{false};
uint8_t last_hit_by_{0xFF}; uint8_t last_hit_by_{0xFF};
// Salut per-instància. Reseteja a config_->health a init(); el dispatcher
// d'events la decrementa via DECREASE_HEALTH i dispara ON_NO_HEALTH quan
// creua zero. Permet enemics tough (HP>1) sense canvis al motor.
int health_{1};
// Flash visual temporitzat per a feedback d'impacte parcial (HP>1).
// L'acció FLASH el reseteja a FLASH_DURATION; draw() alça la lluminositat
// mentre dura, i update() el decrementa.
float flash_timer_{0.0F};
// Métodos privados // Métodos privados
void updateAnimation(float delta_time); void updateAnimation(float delta_time);
void updatePulse(float delta_time); void updatePulse(float delta_time);
+25
View File
@@ -30,6 +30,7 @@ namespace {
if (s == "square") { return EnemyType::SQUARE; } if (s == "square") { return EnemyType::SQUARE; }
if (s == "pinwheel") { return EnemyType::PINWHEEL; } if (s == "pinwheel") { return EnemyType::PINWHEEL; }
if (s == "star") { return EnemyType::STAR; } if (s == "star") { return EnemyType::STAR; }
if (s == "big_pentagon") { return EnemyType::BIG_PENTAGON; }
return std::nullopt; return std::nullopt;
} }
@@ -177,13 +178,25 @@ namespace {
return true; return true;
} }
// health és opcional: si el YAML no l'inclou, el default {1} de l'struct
// ja cobreix el comportament de tots els enemics actuals (1 hit → mort).
void parseHealth(const fkyaml::node& node, int& out) {
if (node.contains("health")) {
out = node["health"].get_value<int>();
}
}
auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> { auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> {
if (s == "set_hurt") { return EnemyActionType::SET_HURT; } if (s == "set_hurt") { return EnemyActionType::SET_HURT; }
if (s == "destroy") { return EnemyActionType::DESTROY; } if (s == "destroy") { return EnemyActionType::DESTROY; }
if (s == "add_score") { return EnemyActionType::ADD_SCORE; } if (s == "add_score") { return EnemyActionType::ADD_SCORE; }
if (s == "create_debris") { return EnemyActionType::CREATE_DEBRIS; } if (s == "create_debris") { return EnemyActionType::CREATE_DEBRIS; }
if (s == "create_debris_partial") { return EnemyActionType::CREATE_DEBRIS_PARTIAL; }
if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; } if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; }
if (s == "create_fireworks_small") { return EnemyActionType::CREATE_FIREWORKS_SMALL; }
if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; } if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; }
if (s == "decrease_health") { return EnemyActionType::DECREASE_HEALTH; }
if (s == "flash") { return EnemyActionType::FLASH; }
return std::nullopt; return std::nullopt;
} }
@@ -340,6 +353,13 @@ namespace {
out.movement.rotation_proximity_multiplier = legacy.rotation_proximity_multiplier; out.movement.rotation_proximity_multiplier = legacy.rotation_proximity_multiplier;
out.movement.proximity_distance = legacy.proximity_distance; out.movement.proximity_distance = legacy.proximity_distance;
break; break;
case EnemyType::BIG_PENTAGON:
// Sense legacy fallback: el YAML del big_pentagon ha de definir
// ai.movement explícitament. Default chase lent perquè el switch
// siga exhaustiu i no falli si algú omet el bloc ai.
out.movement.type = MovementType::CHASE;
out.movement.chase_strength = 0.3F;
break;
} }
} }
@@ -371,6 +391,10 @@ namespace {
if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) { if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) {
return false; return false;
} }
if (e.contains("on_no_health") &&
!parseActionList(e["on_no_health"], name, "on_no_health", out.on_no_health)) {
return false;
}
if (e.contains("on_hurt_end") && if (e.contains("on_hurt_end") &&
!parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) { !parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) {
return false; return false;
@@ -407,6 +431,7 @@ auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; } if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; }
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; } if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; } if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
parseHealth(node, cfg.health);
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; } if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
if (!parseAi(node, cfg.name, cfg.ai_type, cfg.behavior, cfg.ai)) { return std::nullopt; } if (!parseAi(node, cfg.name, cfg.ai_type, cfg.behavior, cfg.ai)) { return std::nullopt; }
+4
View File
@@ -100,6 +100,10 @@ struct EnemyConfig {
SpawnCfg spawn; SpawnCfg spawn;
ColorsCfg colors; ColorsCfg colors;
int score; int score;
// Salut inicial: per defecte 1 (un balazo → on_no_health). Els YAMLs poden
// pujar-lo (p.ex. 10 per a un enemic tough). El sistema d'events és qui
// decideix què passa quan la salut arriba a 0 via on_no_health.
int health{1};
EnemyEventConfig events; EnemyEventConfig events;
EnemyAiConfig ai; EnemyAiConfig ai;
+8
View File
@@ -12,6 +12,7 @@
enum class EnemyEventType : uint8_t { enum class EnemyEventType : uint8_t {
ON_HIT, // Impactat per una bala ON_HIT, // Impactat per una bala
ON_NO_HEALTH, // health ha arribat a 0 o menys aquest frame (via DECREASE_HEALTH)
ON_HURT_END, // Timer wounded ha expirat aquest frame ON_HURT_END, // Timer wounded ha expirat aquest frame
ON_DESTROY, // L'acció destroy s'està executant (efectes col·laterals) ON_DESTROY, // L'acció destroy s'està executant (efectes col·laterals)
}; };
@@ -21,8 +22,12 @@ enum class EnemyActionType : uint8_t {
DESTROY, // Dispara on_destroy + desactiva físicament DESTROY, // Dispara on_destroy + desactiva físicament
ADD_SCORE, // Suma config.score al shooter + floating score ADD_SCORE, // Suma config.score al shooter + floating score
CREATE_DEBRIS, // Explosió de debris amb herència de velocitat CREATE_DEBRIS, // Explosió de debris amb herència de velocitat
CREATE_DEBRIS_PARTIAL, // Debris de xip parcial (trossos a escala 0.3, per hits HP>1)
CREATE_FIREWORKS, // Burst radial de firework CREATE_FIREWORKS, // Burst radial de firework
CREATE_FIREWORKS_SMALL, // Burst petit (pocs punts, poca velocitat) — feedback per hit
APPLY_IMPULSE, // Aplica l'impuls de la bala impactant APPLY_IMPULSE, // Aplica l'impuls de la bala impactant
DECREASE_HEALTH, // Decrementa health_; si <=0, dispatcha ON_NO_HEALTH
FLASH, // Flash visual breu (feedback per impacte parcial)
}; };
struct EnemyAction { struct EnemyAction {
@@ -31,6 +36,7 @@ struct EnemyAction {
struct EnemyEventConfig { struct EnemyEventConfig {
std::vector<EnemyAction> on_hit; std::vector<EnemyAction> on_hit;
std::vector<EnemyAction> on_no_health;
std::vector<EnemyAction> on_hurt_end; std::vector<EnemyAction> on_hurt_end;
std::vector<EnemyAction> on_destroy; std::vector<EnemyAction> on_destroy;
@@ -38,6 +44,8 @@ struct EnemyEventConfig {
switch (event) { switch (event) {
case EnemyEventType::ON_HIT: case EnemyEventType::ON_HIT:
return on_hit; return on_hit;
case EnemyEventType::ON_NO_HEALTH:
return on_no_health;
case EnemyEventType::ON_HURT_END: case EnemyEventType::ON_HURT_END:
return on_hurt_end; return on_hurt_end;
case EnemyEventType::ON_DESTROY: case EnemyEventType::ON_DESTROY:
+6 -2
View File
@@ -13,6 +13,7 @@ EnemyConfig EnemyRegistry::pentagon_config;
EnemyConfig EnemyRegistry::square_config; EnemyConfig EnemyRegistry::square_config;
EnemyConfig EnemyRegistry::pinwheel_config; EnemyConfig EnemyRegistry::pinwheel_config;
EnemyConfig EnemyRegistry::star_config; EnemyConfig EnemyRegistry::star_config;
EnemyConfig EnemyRegistry::big_pentagon_config;
bool EnemyRegistry::loaded = false; bool EnemyRegistry::loaded = false;
namespace { namespace {
@@ -38,10 +39,11 @@ auto EnemyRegistry::loadAll() -> bool {
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) && const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
loadOne("square", EnemyType::SQUARE, square_config) && loadOne("square", EnemyType::SQUARE, square_config) &&
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config) && loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config) &&
loadOne("star", EnemyType::STAR, star_config); loadOne("star", EnemyType::STAR, star_config) &&
loadOne("big_pentagon", EnemyType::BIG_PENTAGON, big_pentagon_config);
loaded = OK; loaded = OK;
if (OK) { if (OK) {
std::cout << "[EnemyRegistry] 4 configuracions d'enemic carregades.\n"; std::cout << "[EnemyRegistry] 5 configuracions d'enemic carregades.\n";
} }
return OK; return OK;
} }
@@ -60,6 +62,8 @@ auto EnemyRegistry::get(EnemyType type) -> const EnemyConfig& {
return pinwheel_config; return pinwheel_config;
case EnemyType::STAR: case EnemyType::STAR:
return star_config; return star_config;
case EnemyType::BIG_PENTAGON:
return big_pentagon_config;
} }
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n"; std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
std::exit(EXIT_FAILURE); std::exit(EXIT_FAILURE);
+1
View File
@@ -27,5 +27,6 @@ class EnemyRegistry {
static EnemyConfig square_config; static EnemyConfig square_config;
static EnemyConfig pinwheel_config; static EnemyConfig pinwheel_config;
static EnemyConfig star_config; static EnemyConfig star_config;
static EnemyConfig big_pentagon_config;
static bool loaded; static bool loaded;
}; };
+7 -3
View File
@@ -580,7 +580,7 @@ auto GameScene::buildCollisionContext() -> Systems::Collision::Context {
.firework_manager = firework_manager_, .firework_manager = firework_manager_,
.floating_score_manager = floating_score_manager_, .floating_score_manager = floating_score_manager_,
.match_config = match_config_, .match_config = match_config_,
.on_player_hit = [this](uint8_t pid) { tocado(pid); }, .on_player_hit = [this](uint8_t pid, const Vec2& bv) { tocado(pid, bv); },
}; };
} }
@@ -764,7 +764,7 @@ void GameScene::drawLevelCompletedState() {
drawScoreboard(); drawScoreboard();
} }
void GameScene::tocado(uint8_t player_id) { void GameScene::tocado(uint8_t player_id, const Vec2& bullet_velocity) {
// Death sequence: 3 phases // Death sequence: 3 phases
// Phase 1: First call (hit_timer_per_player_[player_id] == 0) - trigger explosion // Phase 1: First call (hit_timer_per_player_[player_id] == 0) - trigger explosion
// Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation // Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation
@@ -788,6 +788,9 @@ void GameScene::tocado(uint8_t player_id) {
// Mateixa dispersió i efecte que els debris d'enemic (lifetime, // Mateixa dispersió i efecte que els debris d'enemic (lifetime,
// friction, segment_multiplier alineats); només canvien sound i color. // friction, segment_multiplier alineats); només canvien sound i color.
// bullet_velocity arriba a explode() com a impuls extra independent
// de la inèrcia del cos del ship — els trossos volen amb la força
// de la bala encara que el ship estiga quiet.
debris_manager_.explode( debris_manager_.explode(
ships_[player_id].getShape(), ships_[player_id].getShape(),
SHIP_POS, SHIP_POS,
@@ -802,7 +805,8 @@ void GameScene::tocado(uint8_t player_id) {
ships_[player_id].getConfig().colors.normal, ships_[player_id].getConfig().colors.normal,
Defaults::Physics::Debris::ENEMY_LIFETIME, Defaults::Physics::Debris::ENEMY_LIFETIME,
Defaults::Physics::Debris::ENEMY_FRICTION, Defaults::Physics::Debris::ENEMY_FRICTION,
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER); Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER,
bullet_velocity);
// Start death timer (non-zero to avoid re-triggering) // Start death timer (non-zero to avoid re-triggering)
hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH; hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
+4 -1
View File
@@ -101,7 +101,10 @@ class GameScene final : public Scene {
bool init_hud_rect_sound_played_{false}; // Flag para evitar repetir sonido del rectángulo bool init_hud_rect_sound_played_{false}; // Flag para evitar repetir sonido del rectángulo
// Funciones privades // Funciones privades
void tocado(uint8_t player_id); // bullet_velocity: velocitat de la bala que ha causat la mort (Vec2{} si no
// ve d'una bala). Es passa al debris perquè els fragments volin en direcció
// de la bala (independent de la inèrcia del cos del ship).
void tocado(uint8_t player_id, const Vec2& bullet_velocity = {.x = 0.0F, .y = 0.0F});
void drawScoreboard(); // Dibuixar marcador de puntuación void drawScoreboard(); // Dibuixar marcador de puntuación
void fireBullet(uint8_t player_id); // Shoot bullet from player void fireBullet(uint8_t player_id); // Shoot bullet from player
[[nodiscard]] auto getSpawnPoint(uint8_t player_id) const -> Vec2; // Get spawn position for player [[nodiscard]] auto getSpawnPoint(uint8_t player_id) const -> Vec2; // Get spawn position for player
@@ -129,18 +129,22 @@ namespace StageSystem {
// Weighted random selection based on distribution // Weighted random selection based on distribution
int rand_val = std::rand() % 100; int rand_val = std::rand() % 100;
const auto& d = config_->distribucio;
if (std::cmp_less(rand_val, config_->distribucio.pentagon)) { if (std::cmp_less(rand_val, d.pentagon)) {
return EnemyType::PENTAGON; return EnemyType::PENTAGON;
} }
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) { if (rand_val < d.pentagon + d.cuadrado) {
return EnemyType::SQUARE; return EnemyType::SQUARE;
} }
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado + config_->distribucio.molinillo) { if (rand_val < d.pentagon + d.cuadrado + d.molinillo) {
return EnemyType::PINWHEEL; return EnemyType::PINWHEEL;
} }
if (rand_val < d.pentagon + d.cuadrado + d.molinillo + d.star) {
return EnemyType::STAR; return EnemyType::STAR;
} }
return EnemyType::BIG_PENTAGON;
}
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) { void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
// Initialize enemy (with safe spawn if ship_pos provided) // Initialize enemy (with safe spawn if ship_pos provided)
+2 -1
View File
@@ -29,6 +29,7 @@ namespace StageSystem {
uint8_t cuadrado; // 0-100 uint8_t cuadrado; // 0-100
uint8_t molinillo; // 0-100 uint8_t molinillo; // 0-100
uint8_t star{0}; // 0-100 (opcional al YAML; default 0 per compat amb stages antics) uint8_t star{0}; // 0-100 (opcional al YAML; default 0 per compat amb stages antics)
uint8_t big_pentagon{0}; // 0-100 (opcional; enemic gegant HP=10)
// Suma ha de ser 100, validat en StageLoader // Suma ha de ser 100, validat en StageLoader
}; };
@@ -60,7 +61,7 @@ namespace StageSystem {
// el tipo; basta con confirmar que no es 0 (sentinela "sin asignar"). // el tipo; basta con confirmar que no es 0 (sentinela "sin asignar").
return stage_id >= 1 && return stage_id >= 1 &&
total_enemies > 0 && total_enemies <= 200 && total_enemies > 0 && total_enemies <= 200 &&
distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo + distribucio.star == 100; distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo + distribucio.star + distribucio.big_pentagon == 100;
} }
}; };
+3 -2
View File
@@ -171,11 +171,12 @@ namespace StageSystem {
dist.pentagon = yaml["pentagon"].get_value<uint8_t>(); dist.pentagon = yaml["pentagon"].get_value<uint8_t>();
dist.cuadrado = yaml["cuadrado"].get_value<uint8_t>(); dist.cuadrado = yaml["cuadrado"].get_value<uint8_t>();
dist.molinillo = yaml["molinillo"].get_value<uint8_t>(); dist.molinillo = yaml["molinillo"].get_value<uint8_t>();
// 'star' és opcional per compatibilitat amb stages antics (default 0). // 'star' i 'big_pentagon' són opcionals per compatibilitat amb stages antics (default 0).
dist.star = yaml.contains("star") ? yaml["star"].get_value<uint8_t>() : 0; dist.star = yaml.contains("star") ? yaml["star"].get_value<uint8_t>() : 0;
dist.big_pentagon = yaml.contains("big_pentagon") ? yaml["big_pentagon"].get_value<uint8_t>() : 0;
// Validar que suma 100 // Validar que suma 100
int sum = dist.pentagon + dist.cuadrado + dist.molinillo + dist.star; int sum = dist.pentagon + dist.cuadrado + dist.molinillo + dist.star + dist.big_pentagon;
if (sum != 100) { if (sum != 100) {
std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n'; std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n';
return false; return false;
+23 -45
View File
@@ -83,36 +83,6 @@ namespace Systems::Collision {
} }
} }
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) { void detectShipEnemy(Context& ctx) {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
@@ -150,7 +120,9 @@ namespace Systems::Collision {
const float DEATH_FACTOR = ctx.ships[i].getConfig().physics.death_impact_factor; const float DEATH_FACTOR = ctx.ships[i].getConfig().physics.death_impact_factor;
const Vec2 IMPULSE = SHIP_VEL * (ctx.ships[i].getBody().mass * DEATH_FACTOR); const Vec2 IMPULSE = SHIP_VEL * (ctx.ships[i].getBody().mass * DEATH_FACTOR);
touched_enemy->applyImpulse(IMPULSE); touched_enemy->applyImpulse(IMPULSE);
ctx.on_player_hit(i); // 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 { } else {
// Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld; // Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld;
// l'enemic no rep dany per decisió de disseny). // l'enemic no rep dany per decisió de disseny).
@@ -197,13 +169,12 @@ namespace Systems::Collision {
} }
// *** TEAMMATE HIT (friendly fire) *** // *** TEAMMATE HIT (friendly fire) ***
// Víctima perd 1 vida, atacant en guanya 1. Apliquem l'impuls // Víctima perd 1 vida, atacant en guanya 1. Friendly fire sempre
// de la bala a la nau ABANS de on_player_hit perquè tocado() // mata: el bullet va als debris (via tocado) i NO al cos del ship
// captura la velocitat per als debris (si no, queden quiets). // — el cos està a punt de desactivar-se, qualsevol impuls seria
const Vec2 BULLET_IMPULSE = bullet.getBody().velocity * // double-count amb la velocitat que ja reben els trossos.
(bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor); const Vec2 BULLET_VEL = bullet.getBody().velocity;
ctx.ships[player_id].getBody().applyImpulse(BULLET_IMPULSE); ctx.on_player_hit(player_id, BULLET_VEL);
ctx.on_player_hit(player_id);
ctx.lives_per_player[BULLET_OWNER]++; ctx.lives_per_player[BULLET_OWNER]++;
Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME);
breakBullet(ctx.debris_manager, bullet); breakBullet(ctx.debris_manager, bullet);
@@ -237,15 +208,20 @@ namespace Systems::Collision {
} }
// *** BALA D'ENEMIC → SHIP *** // *** BALA D'ENEMIC → SHIP ***
// Apliquem l'impuls de la bala abans del hurt/death perquè la // Regla "cos XOR trossos": l'impuls de la bala s'aplica al cos
// velocitat de la nau quedi capturada per als debris. // només si el ship sobreviu (fereix). Si el ship mor, el bullet
const Vec2 IMPULSE = bullet.getBody().velocity * // va directament als trossos (via tocado) i el cos no rep impuls
(bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor); // — els trossos ja porten la força de la bala, qualsevol impuls
ctx.ships[player_id].getBody().applyImpulse(IMPULSE); // afegit al cos seria double-count.
const Vec2 BULLET_VEL = bullet.getBody().velocity;
if (ctx.ships[player_id].isHurt()) { if (ctx.ships[player_id].isHurt()) {
// Segon impacte durant HURT → mort. // Segon impacte durant HURT → mort.
ctx.on_player_hit(player_id); ctx.on_player_hit(player_id, BULLET_VEL);
} else { } 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(); ctx.ships[player_id].hurt();
} }
breakBullet(ctx.debris_manager, bullet); breakBullet(ctx.debris_manager, bullet);
@@ -257,7 +233,9 @@ namespace Systems::Collision {
void detectAll(Context& ctx) { void detectAll(Context& ctx) {
processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame
detectBulletEnemy(ctx); detectBulletEnemy(ctx);
detectWoundedChain(ctx); // un herit pot ferir a un sa al fregar-lo // Wounded chain desactivat: era massa fàcil que un enemic ferit topés
// amb el big_pentagon (10 HP) i el matés instantàniament. La regla
// "ferit-toca-sa → ferit" queda permanentment fora.
detectShipEnemy(ctx); detectShipEnemy(ctx);
detectBulletPlayer(ctx); detectBulletPlayer(ctx);
detectEnemyBulletShip(ctx); detectEnemyBulletShip(ctx);
+5 -8
View File
@@ -40,8 +40,11 @@ namespace Systems::Collision {
Effects::FireworkManager& firework_manager; Effects::FireworkManager& firework_manager;
Effects::FloatingScoreManager& floating_score_manager; Effects::FloatingScoreManager& floating_score_manager;
const GameConfig::MatchConfig& match_config; const GameConfig::MatchConfig& match_config;
// Trigger de muerte del jugador (GameScene::tocado). // Trigger de muerte del jugador (GameScene::tocado). bullet_velocity es
std::function<void(uint8_t /*player_id*/)> on_player_hit; // la velocitat de la bala que ha causat la mort (Vec2{} si la mort no
// ve d'una bala — col·lisió ship-enemy, etc.). Es passa al debris perquè
// els trossos volin en direcció de la bala.
std::function<void(uint8_t /*player_id*/, const Vec2& /*bullet_velocity*/)> on_player_hit;
}; };
// Detecta colisiones bullet → enemy. Si hit: // Detecta colisiones bullet → enemy. Si hit:
@@ -53,12 +56,6 @@ namespace Systems::Collision {
// al `last_hit_by_` del enemy (si está set). // al `last_hit_by_` del enemy (si está set).
void processWoundedDeaths(Context& ctx); void processWoundedDeaths(Context& ctx);
// Si un enemy herido colisiona con uno sano (ni herido ni invulnerable),
// el sano también queda herido (efecto cadena). Propaga `last_hit_by_` para
// que el shooter original siga acreditándose la muerte en cascada. El rebote
// físico ya lo resuelve PhysicsWorld; aquí solo propagamos el estado.
void detectWoundedChain(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);
+69 -10
View File
@@ -24,10 +24,14 @@ namespace Systems::EnemyEvents {
ctx.floating_score_manager.crear(POINTS, enemy.getCenter()); ctx.floating_score_manager.crear(POINTS, enemy.getCenter());
} }
void doCreateDebris(Systems::Collision::Context& ctx, const Enemy& enemy) { // Helper compartit per CREATE_DEBRIS i CREATE_DEBRIS_PARTIAL: única
// crida a explode(), paràmetres alineats; només canvien piece_scale
// (1.0 explosió, 0.3 xip) i el color (cos vs hit-feedback).
void spawnDebrisForEnemy(Systems::Collision::Context& ctx, const Enemy& enemy, const Bullet* bullet, float piece_scale, SDL_Color color) {
constexpr float SPEED_EXPLOSIO = 80.0F; constexpr float SPEED_EXPLOSIO = 80.0F;
const Vec2 INHERITED_VEL = enemy.getVelocityVector() * const Vec2 INHERITED_VEL = enemy.getVelocityVector() *
Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE; Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
const Vec2 BULLET_VEL = (bullet != nullptr) ? bullet->getBody().velocity : Vec2{};
ctx.debris_manager.explode( ctx.debris_manager.explode(
enemy.getShape(), enemy.getShape(),
enemy.getCenter(), enemy.getCenter(),
@@ -39,20 +43,25 @@ namespace Systems::EnemyEvents {
0.0F, 0.0F,
0.0F, 0.0F,
Defaults::Sound::EXPLOSION, Defaults::Sound::EXPLOSION,
enemy.getConfig().colors.normal, color,
Defaults::Physics::Debris::ENEMY_LIFETIME, Defaults::Physics::Debris::ENEMY_LIFETIME,
Defaults::Physics::Debris::ENEMY_FRICTION, Defaults::Physics::Debris::ENEMY_FRICTION,
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER); Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER,
BULLET_VEL,
piece_scale);
} }
void doCreateFireworks(Systems::Collision::Context& ctx, const Enemy& enemy) { // Helper compartit per CREATE_FIREWORKS i CREATE_FIREWORKS_SMALL:
// mateixa crida a spawn(); els callers decideixen line_color, glow_color,
// n_points i initial_speed segons el "tamany" del burst (mort vs hit).
void spawnFireworksForEnemy(Systems::Collision::Context& ctx, const Enemy& enemy, int n_points, float initial_speed, SDL_Color line_color, SDL_Color glow_color) {
ctx.firework_manager.spawn(enemy.getCenter(), ctx.firework_manager.spawn(enemy.getCenter(),
Defaults::FX::Firework::DEFAULT_COLOR, line_color,
Defaults::FX::Firework::SPEED, initial_speed,
Defaults::FX::Firework::N_POINTS, n_points,
Defaults::FX::Firework::INITIAL_BRIGHTNESS, Defaults::FX::Firework::INITIAL_BRIGHTNESS,
/*glow=*/true, /*glow=*/true,
enemy.getConfig().colors.wounded); glow_color);
} }
void doApplyImpulse(Enemy& enemy, const Bullet* bullet) { void doApplyImpulse(Enemy& enemy, const Bullet* bullet) {
@@ -67,7 +76,32 @@ namespace Systems::EnemyEvents {
void dispatchEvent(Systems::Collision::Context& ctx, Enemy& enemy, EnemyEventType event, uint8_t shooter_id, const Bullet* bullet) { void dispatchEvent(Systems::Collision::Context& ctx, Enemy& enemy, EnemyEventType event, uint8_t shooter_id, const Bullet* bullet) {
const auto& actions = enemy.getConfig().events.getActions(event); const auto& actions = enemy.getConfig().events.getActions(event);
// Pre-scan: aquest event matarà l'enemic? Si sí, l'impuls de la bala
// va directament als debris (via doCreateDebris) i NO s'aplica al cos
// — així evitem el "double-count" on els trossos hereten la velocitat
// del cos (boostat per la bala) I a més el seu propi impuls de bala.
// Regla: el bullet impacta al cos O als trossos, mai a tots dos.
bool will_die = false;
for (const auto& action : actions) { for (const auto& action : actions) {
if (action.type == EnemyActionType::DESTROY) {
will_die = true;
break;
}
if (action.type == EnemyActionType::SET_HURT && enemy.isWounded()) {
will_die = true;
break;
}
}
for (const auto& action : actions) {
// Si una acció prèvia d'aquest chain ha destruït l'enemic
// (típicament DECREASE_HEALTH→ON_NO_HEALTH→SET_HURT-wounded→DESTROY),
// saltem la resta — no té sentit aplicar APPLY_IMPULSE o FLASH a un
// cos ja inactiu.
if (!enemy.isActive()) {
break;
}
switch (action.type) { switch (action.type) {
case EnemyActionType::SET_HURT: case EnemyActionType::SET_HURT:
if (enemy.isWounded()) { if (enemy.isWounded()) {
@@ -86,13 +120,38 @@ namespace Systems::EnemyEvents {
doAddScore(ctx, enemy, shooter_id); doAddScore(ctx, enemy, shooter_id);
break; break;
case EnemyActionType::CREATE_DEBRIS: case EnemyActionType::CREATE_DEBRIS:
doCreateDebris(ctx, enemy); // Explosió de mort: trossos en color cos (correcte físicament).
spawnDebrisForEnemy(ctx, enemy, bullet, 1.0F, enemy.getConfig().colors.normal);
break;
case EnemyActionType::CREATE_DEBRIS_PARTIAL:
// Xip d'impacte: trossos en color wounded (daurat) per
// diferenciar-los visualment del cos i marcar "damage".
spawnDebrisForEnemy(ctx, enemy, bullet, Defaults::Enemies::Debris::PARTIAL_PIECE_SCALE, enemy.getConfig().colors.wounded);
break; break;
case EnemyActionType::CREATE_FIREWORKS: case EnemyActionType::CREATE_FIREWORKS:
doCreateFireworks(ctx, enemy); // Burst de mort: línia blanca + glow wounded (daurat) per
// marcar la mort com a esdeveniment "calent" i lluminós.
spawnFireworksForEnemy(ctx, enemy, Defaults::FX::Firework::N_POINTS, Defaults::FX::Firework::SPEED, Defaults::FX::Firework::DEFAULT_COLOR, enemy.getConfig().colors.wounded);
break;
case EnemyActionType::CREATE_FIREWORKS_SMALL:
// Espurna d'impacte: línia + glow tots dos en wounded
// (daurat) per contrastar amb el cos i unificar la "tema
// de damage" amb el debris parcial.
spawnFireworksForEnemy(ctx, enemy, Defaults::Enemies::Fireworks::SMALL_N_POINTS, Defaults::Enemies::Fireworks::SMALL_SPEED, enemy.getConfig().colors.wounded, enemy.getConfig().colors.wounded);
break; break;
case EnemyActionType::APPLY_IMPULSE: case EnemyActionType::APPLY_IMPULSE:
if (!will_die) {
doApplyImpulse(enemy, bullet); doApplyImpulse(enemy, bullet);
}
break;
case EnemyActionType::DECREASE_HEALTH:
enemy.decrementHealth(shooter_id);
if (enemy.getHealth() <= 0) {
dispatchEvent(ctx, enemy, EnemyEventType::ON_NO_HEALTH, shooter_id, bullet);
}
break;
case EnemyActionType::FLASH:
enemy.triggerFlash();
break; break;
} }
} }