Compare commits

...

7 Commits

Author SHA1 Message Date
JailDesigner 9c0502eefb feat(enemy): sistema d'events declaratius via YAML 2026-05-25 13:34:48 +02:00
JailDesigner 9b3da3a6e7 Merge branch 'feat/enemy-star': afegir tipus STAR i 3 nous shapes 2026-05-25 12:42:06 +02:00
JailDesigner bc41169176 feat(enemy): afegir tipus STAR (estrella de 5 puntes) i 3 nous shapes
- Nou enemic STAR amb shape star_5.shp, escala 0.7 i color groc pur.
  Reusa el comportament zigzag del Pentagon i carrega via EnemyRegistry.
- DistribucioEnemics estesa amb camp 'star' opcional (default 0) per
  mantenir compat amb stages antics.
- Stage 1 reconfigurat a 25/25/25/25 per mostrar els 4 tipus.
- Afegits també shapes bullet_long.shp i bullet_double.shp (encara no
  utilitzats; preparats per futures variants de bala).
2026-05-25 12:36:26 +02:00
JailDesigner b3a1afce06 Merge branch 'feat/entities-yaml-enemy-shared': paràmetres compartits dels enemics a cada YAML 2026-05-25 11:59:28 +02:00
JailDesigner 4b6dc8a47a feat(entities): migrar paràmetres compartits dels enemics a cada YAML 2026-05-25 11:54:40 +02:00
JailDesigner 3dadd5fc1a Merge branch 'feat/entities-yaml-bullet': migració de la bala a YAML 2026-05-25 11:47:36 +02:00
JailDesigner bea844d51e feat(entities): migrar bullet a data/entities/bullet/bullet.yaml 2026-05-25 11:42:43 +02:00
33 changed files with 1228 additions and 545 deletions
+22
View File
@@ -0,0 +1,22 @@
name: bullet
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
# scale el modula visualment i pel hitbox.
shape:
path: bullet.shp
scale: 1.0
collision_factor: 1.0
# Cinemàtica pura: la bala no col·lisiona físicament al PhysicsWorld
# (body_.radius = 0 al spawn), però sí participa al gameplay via
# checkCollisionSwept. La mass i l'impact_momentum_factor es fan servir
# només per calcular l'impuls que rep l'enemic en impactar.
physics:
mass: 0.5
restitution: 0.0 # irrelevant (no rebota)
linear_damping: 0.0 # movement rectilini uniforme
angular_damping: 0.0
impact_momentum_factor: 3.0 # factor de transferència de moment bala→enemic
colors:
normal: [155, 255, 175] # verd laser
+44
View File
@@ -11,14 +11,58 @@ physics:
speed: 35.0 # px/s (esquivador lent) speed: 35.0 # px/s (esquivador lent)
rotation_delta_min: 0.75 # rad/s — rotació visual mínima rotation_delta_min: 0.75 # rad/s — rotació visual mínima
rotation_delta_max: 3.75 # rad/s — rotació visual màxima rotation_delta_max: 3.75 # rad/s — rotació visual màxima
restitution: 1.0 # rebot elàstic perfecte contra parets
linear_damping: 0.0 # manté velocitat (sense fricció)
angular_damping: 0.0
behavior: behavior:
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon). # Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
angle_change_max: 1.0 # rad — magnitud del canvi de direcció angle_change_max: 1.0 # rad — magnitud del canvi de direcció
zigzag_prob_per_second: 0.8 zigzag_prob_per_second: 0.8
animation:
pulse: # respiració d'escala aleatòria
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: # acceleració/desacceleració de rotació visual
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.0 # segons en estat ferit abans d'explotar
blink_hz: 10.0 # parpelleig color normal ↔ wounded
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: 36.0 # px mínim respecte al player al spawn
colors: colors:
normal: [0, 255, 255] # Cyan pur "esquivador" normal: [0, 255, 255] # Cyan pur "esquivador"
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte) wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
score: 100 score: 100
events:
# Comportament clàssic: dos impactes per matar (set_hurt entra wounded;
# el segon hit detecta wounded i destrueix automàticament).
on_hit:
- action: apply_impulse
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+42
View File
@@ -11,14 +11,56 @@ physics:
speed: 50.0 # px/s (el més ràpid) speed: 50.0 # px/s (el més ràpid)
rotation_delta_min: 3.0 # rad/s — rotació base elevada rotation_delta_min: 3.0 # rad/s — rotació base elevada
rotation_delta_max: 6.0 rotation_delta_max: 6.0
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior: behavior:
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau. # Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau
proximity_distance: 100.0 # Llindar de distància (px) proximity_distance: 100.0 # Llindar de distància (px)
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.0
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: 36.0
colors: colors:
normal: [255, 0, 255] # Magenta pur "agressiu" normal: [255, 0, 255] # Magenta pur "agressiu"
wounded: [255, 220, 60] wounded: [255, 220, 60]
score: 200 score: 200
events:
on_hit:
- action: apply_impulse
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+42
View File
@@ -11,14 +11,56 @@ physics:
speed: 40.0 # px/s (velocitat mitjana) speed: 40.0 # px/s (velocitat mitjana)
rotation_delta_min: 0.3 # rad/s — rotació lenta rotation_delta_min: 0.3 # rad/s — rotació lenta
rotation_delta_max: 1.5 rotation_delta_max: 1.5
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior: behavior:
# Square: tracking discret cap a la nau cada N segons. # Square: tracking discret cap a la nau cada N segons.
tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1) tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1)
tracking_interval: 1.0 # segons entre updates d'angle tracking_interval: 1.0 # segons entre updates d'angle
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.0
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: 36.0
colors: colors:
normal: [255, 0, 0] # Roig pur "tanc" normal: [255, 0, 0] # Roig pur "tanc"
wounded: [255, 220, 60] wounded: [255, 220, 60]
score: 150 score: 150
events:
on_hit:
- action: apply_impulse
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+65
View File
@@ -0,0 +1,65 @@
name: star
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
shape:
path: star_5.shp
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
collision_factor: 1.0
physics:
mass: 5.0
speed: 35.0 # Mateixos paràmetres que pentagon (esquivador lent).
rotation_delta_min: 0.75
rotation_delta_max: 3.75
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior:
# Hereta el comportament de Pentagon (zigzag esquivador).
angle_change_max: 1.0
zigzag_prob_per_second: 0.8
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.0
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: 36.0
colors:
normal: [255, 255, 0] # Groc estrella
wounded: [255, 220, 60]
score: 100
events:
# STAR: mor al primer impacte, sense passar per wounded.
on_hit:
- action: apply_impulse
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+17
View File
@@ -0,0 +1,17 @@
# bullet_double.shp - Bala anular (dos cercles concèntrics)
# © 2026 JailDesigner
#
# Dos octàgons concèntrics al centre (0,0):
# - Exterior: radi 4 (lleugerament més gran que la bala estàndard, radi 3)
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
name: bullet_double
scale: 1.0
center: 0, 0
# Cercle exterior (octàgon, radi 4)
polyline: 0,-4 2.83,-2.83 4,0 2.83,2.83 0,4 -2.83,2.83 -4,0 -2.83,-2.83 0,-4
# Cercle interior (octàgon, radi 2)
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
+28
View File
@@ -0,0 +1,28 @@
# bullet_long.shp - Bala allargada (dos octàgons tangents + tapes superior i inferior)
# © 2026 JailDesigner
#
# Dos cercles (octàgons radi 3) tangents externament al punt (0,0), units
# per una línia horitzontal superior i una d'inferior. La silueta resultant
# és una càpsula amb la separació visible dels dos cercles al centre.
#
# Geometria:
# Centre octàgon esquerre: (-3, 0)
# Centre octàgon dret: ( 3, 0)
# Punt de tangència: ( 0, 0)
# Bounding radius natiu ≈ 6 (extrem horitzontal a x=±6).
name: bullet_long
scale: 1.0
center: 0, 0
# Octàgon esquerre (centre x=-3, radi 3)
polyline: -3,-3 -0.88,-2.12 0,0 -0.88,2.12 -3,3 -5.12,2.12 -6,0 -5.12,-2.12 -3,-3
# Octàgon dret (centre x=3, radi 3)
polyline: 3,-3 5.12,-2.12 6,0 5.12,2.12 3,3 0.88,2.12 0,0 0.88,-2.12 3,-3
# Tapa superior: uneix el cim de l'octàgon esquerre amb el del dret
polyline: -3,-3 3,-3
# Tapa inferior: uneix la base de l'octàgon esquerre amb la del dret
polyline: -3,3 3,3
+15
View File
@@ -0,0 +1,15 @@
# star_5.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
# © 2026 JailDesigner
#
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
# interiors (radi 7.64 = 20/φ² ≈ proporció àuria) per donar puntes esveltes.
# Vèrtex apuntant amunt (igual que enemy_pentagon).
#
# Sense línies interiors: una única polyline que recorre el perímetre.
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
name: star_5
scale: 1.0
center: 0, 0
polyline: 0,-20 4.49,-6.18 19.02,-6.18 7.27,2.36 11.76,16.18 0,7.64 -11.76,16.18 -7.27,2.36 -19.02,-6.18 -4.49,-6.18 0,-20
+5 -4
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 els tipus, velocitat lenta # STAGE 1: Tutorial - Mix de tots 4 tipus al 25% per mostrar-los junts
- stage_id: 1 - stage_id: 1
total_enemies: 50 total_enemies: 50
spawn_config: spawn_config:
@@ -15,9 +15,10 @@ stages:
initial_delay: 0.3 initial_delay: 0.3
spawn_interval: 0.4 spawn_interval: 0.4
enemy_distribution: enemy_distribution:
pentagon: 34 pentagon: 25
cuadrado: 33 cuadrado: 25
molinillo: 33 molinillo: 25
star: 25
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 0.7 speed_multiplier: 0.7
rotation_multiplier: 0.8 rotation_multiplier: 0.8
+12 -58
View File
@@ -1,64 +1,18 @@
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Square/Molinillo), spawn i scoring // enemies.hpp - Constants tècniques compartides per al sistema d'enemics.
// © 2026 JailDesigner // © 2026 JailDesigner
//
// Tots els paràmetres jugables (physics, animation, wounded, spawn,
// behavior, colors, scoring) viuen a data/entities/<type>/<type>.yaml i
// s'accedeixen via EnemyRegistry::get(EnemyType). Aquí només queda el
// que no és per personalitzar per tipus.
#pragma once #pragma once
namespace Defaults::Enemies { namespace Defaults::Enemies::Spawn {
// Cuerpo físico común (valores por defecto del constructor) // Sostre de reintents al cercar una posició de spawn que respecti el
namespace Body { // safety_distance del tipus. No és un paràmetre jugable: és el llindar
constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0) // tècnic abans de caure a un fallback aleatori amb advertència.
constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes constexpr int MAX_SPAWN_ATTEMPTS = 50;
constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad
constexpr float ANGULAR_DAMPING = 0.0F;
} // namespace Body
// NOTA: els paràmetres per tipus (Pentagon/Square/Pinwheel) i el Scoring } // namespace Defaults::Enemies::Spawn
// viuen ara a data/entities/{pentagon,square,pinwheel}/*.yaml i s'accedeixen
// via EnemyRegistry::get(EnemyType). Aquí només queden els paràmetres
// compartits entre tots els tipus (animació, wounded, spawn).
// Animation parameters (shared)
namespace Animation {
// Palpitation
constexpr float PULSE_TRIGGER_PROB = 0.01F; // 1% chance per second
constexpr float PULSE_DURATION_MIN = 1.0F; // Min duration (seconds)
constexpr float PULSE_DURATION_MAX = 3.0F; // Max duration (seconds)
constexpr float PULSE_AMPLITUD_MIN = 0.08F; // Min scale variation
constexpr float PULSE_AMPLITUD_MAX = 0.20F; // Max scale variation
constexpr float PULSE_FREQ_MIN = 1.5F; // Min frequency (Hz)
constexpr float PULSE_FREQ_MAX = 3.0F; // Max frequency (Hz)
// Rotation acceleration
constexpr float ROTATION_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent]
constexpr float ROTATION_ACCEL_DURATION_MIN = 3.0F; // Min transition time
constexpr float ROTATION_ACCEL_DURATION_MAX = 8.0F; // Max transition time
constexpr float ROTATION_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
constexpr float ROTATION_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
} // namespace Animation
// Wounded state (entre primer impacto y explosión)
namespace Wounded {
constexpr float DURATION = 1.0F; // Segundos en estado herido antes de explotar
constexpr float BLINK_HZ = 10.0F; // Frecuencia de parpadeo color tipo ↔ dorado
} // namespace Wounded
// Spawn safety and invulnerability system
namespace Spawn {
// Safe spawn distance from player. Antic: SHIP_RADIUS(12) * 3 = 36 px.
// SHIP_RADIUS ha migrat al YAML del player; aquesta constant es
// mantindrà fixa fins al PR de migració dels enemics a YAML, on
// passarà a derivar-se en runtime del player_config.
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F;
constexpr float SAFETY_DISTANCE = 36.0F;
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
// Invulnerability system
constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC)
constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible
constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size
} // namespace Spawn
} // namespace Defaults::Enemies
+4 -3
View File
@@ -8,8 +8,9 @@ namespace Defaults::Entities {
constexpr int MAX_ORNIS = 15; constexpr int MAX_ORNIS = 15;
constexpr int MAX_BULLETS = 50; constexpr int MAX_BULLETS = 50;
// SHIP_RADIUS migrat a data/entities/player/player.yaml (physics.collision_radius). // SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
// ENEMY_RADIUS migrat a data/entities/<type>/<type>.yaml (physics.collision_radius). // calcula el seu collision_radius com a
constexpr float BULLET_RADIUS = 3.0F; // shape.bounding_radius × shape.scale × shape.collision_factor
// a partir del seu YAML (data/entities/<name>/<name>.yaml).
} // namespace Defaults::Entities } // namespace Defaults::Entities
+5 -5
View File
@@ -14,10 +14,10 @@ namespace Defaults::Palette {
// brillantor perceptual sota el bloom (sense alterar la identitat de color). // brillantor perceptual sota el bloom (sense alterar la identitat de color).
// El canal dominant es manté a 255 a cada color per maximitzar la saturació // El canal dominant es manté a 255 a cada color per maximitzar la saturació
// visible quan el halo s'expandeix. // visible quan el halo s'expandeix.
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser // Tots els colors d'entitats han migrat al seu YAML respectiu
// SHIP s'ha migrat a data/entities/player/player.yaml (colors.normal). // (data/entities/<name>/<name>.yaml, secció `colors`):
// PENTAGON, SQUARE, PINWHEEL i WOUNDED han migrat a cada enemy YAML // - SHIP → player.yaml
// (colors.normal i colors.wounded). // - PENTAGON / SQUARE / PINWHEEL / WOUNDED → cada enemy.yaml
// BULLET es queda compartit fins a la migració del bullet a YAML. // - BULLET → bullet.yaml
} // namespace Defaults::Palette } // namespace Defaults::Palette
+8 -17
View File
@@ -3,23 +3,15 @@
#pragma once #pragma once
namespace Defaults::Physics { // NOTA: els paràmetres del player (rotation_speed, acceleration,
// max_velocity, death_impact_factor) viuen a data/entities/player/player.yaml.
// Els paràmetres específics de la bala (mass, restitution, damping,
// impact_momentum_factor) viuen a data/entities/bullet/bullet.yaml.
// Aquest fitxer només conté els paràmetres compartits del subsistema de
// debris (explosions visuals).
// NOTA: els paràmetres específics de la nau del player (rotation_speed, namespace Defaults::Physics::Debris {
// acceleration, max_velocity, death_impact_factor) viuen ara a
// data/entities/player/player.yaml. La migració d'aquests fitxers va
// començar amb la nau; els enemics i les bales són els següents.
// Bullet — impacto físico contra enemigo (impulse mass-aware).
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat.
namespace Bullet {
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
} // namespace Bullet
// Explosions (debris physics)
namespace Debris {
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s) constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s) constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²) constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
@@ -61,6 +53,5 @@ namespace Defaults::Physics {
// 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
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s) constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
} // namespace Debris
} // namespace Defaults::Physics } // namespace Defaults::Physics::Debris
+22 -32
View File
@@ -14,38 +14,39 @@
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
#include "game/entities/bullet_config.hpp"
#include "game/entities/bullet_registry.hpp"
Bullet::Bullet(Rendering::Renderer* renderer) Bullet::Bullet(Rendering::Renderer* renderer)
: Entity(renderer) { : Entity(renderer),
// Brightness específico para balas config_(&BulletRegistry::get()) {
brightness_ = Defaults::Brightness::BALA; brightness_ = Defaults::Brightness::BALA;
// Configuración del cuerpo físico. // Cinemàtiques pures: no col·lisionen al PhysicsWorld (body_.radius = 0).
// Las balas son cinemáticas: no colisionan con otros bodies ni paredes. // El gameplay (GameScene) gestiona els hits via checkCollisionSwept i la
// El gameplay (GameScene) gestiona los hits con check_collision y la // sortida del PLAYAREA.
// salida del PLAYAREA. Por eso radius=0 en el world (no participa en body_.setMass(config_->physics.mass);
// resolveBodyCollisions ni resolveBoundsCollisions). body_.radius = 0.0F;
body_.setMass(0.5F); // Ligera (no afecta a nadie, pero por consistencia) body_.restitution = config_->physics.restitution;
body_.radius = 0.0F; // Sin colisión física (cinemática pura) body_.linear_damping = config_->physics.linear_damping;
body_.restitution = 0.0F; // Irrelevante (no rebota) body_.angular_damping = config_->physics.angular_damping;
body_.linear_damping = 0.0F; // Sin fricción (movimiento rectilíneo uniforme)
body_.angular_damping = 0.0F;
// Cargar shape compartida desde archivo shape_ = Graphics::ShapeLoader::load(config_->shape.path);
shape_ = Graphics::ShapeLoader::load("bullet.shp");
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
std::cerr << "[Bullet] Error: no s'ha pogut load bullet.shp" << '\n'; std::cerr << "[Bullet] Error: no s'ha pogut carregar " << config_->shape.path << '\n';
} }
// Radi de col·lisió derivat del cercle circumscrit de la shape.
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
collision_radius_ = BOUNDING * config_->shape.scale * config_->shape.collision_factor;
} }
void Bullet::init() { void Bullet::init() {
// Inicialment inactiva
is_active_ = false; is_active_ = false;
center_ = {.x = 0.0F, .y = 0.0F}; center_ = {.x = 0.0F, .y = 0.0F};
prev_position_ = {.x = 0.0F, .y = 0.0F}; prev_position_ = {.x = 0.0F, .y = 0.0F};
angle_ = 0.0F; angle_ = 0.0F;
// Reset del cuerpo físico
body_.position = Vec2{}; body_.position = Vec2{};
body_.velocity = Vec2{}; body_.velocity = Vec2{};
body_.angle = 0.0F; body_.angle = 0.0F;
@@ -54,19 +55,15 @@ void Bullet::init() {
} }
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed) { void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed) {
// Activar bullet
is_active_ = true; is_active_ = true;
// Almacenar propietario (0=P1, 1=P2)
owner_id_ = owner_id; owner_id_ = owner_id;
// Posición y orientación iniciales = ship
center_ = position; center_ = position;
prev_position_ = position; // Al spawn no hi ha moviment encara: swept degenera a punt-cercle prev_position_ = position; // spawn: swept degenera a punt-cercle
angle_ = angle; angle_ = angle;
// Sincronizar el body físico: posición + velocidad cartesiana // Sincronizar el body físic: posició + velocitat cartesiana.
// angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL) // angle - PI/2 perquè angle=0 apunta cap amunt (eje Y negatiu SDL).
body_.position = position; body_.position = position;
body_.angle = angle; body_.angle = angle;
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F)); const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
@@ -75,7 +72,6 @@ void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bul
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.clearAccumulators(); body_.clearAccumulators();
// Reproducir sonido de disparo láser
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
} }
@@ -87,24 +83,18 @@ void Bullet::update(float /*delta_time*/) {
} }
void Bullet::postUpdate(float /*delta_time*/) { void Bullet::postUpdate(float /*delta_time*/) {
// Captura la posició al final del frame anterior abans de sobreescriure center_;
// així el sistema de col·lisions pot fer swept (segment-vs-cercle) entre prev_position_
// i la nova center_, evitant tunneling a velocitats altes.
prev_position_ = center_; prev_position_ = center_;
center_ = body_.position; center_ = body_.position;
// angle_ no cambia (las balas no rotan visualmente).
} }
void Bullet::desactivar() { void Bullet::desactivar() {
is_active_ = false; is_active_ = false;
// Detener el cuerpo físico para que no acumule deriva mientras inactiva.
body_.velocity = Vec2{}; body_.velocity = Vec2{};
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
} }
void Bullet::draw() const { void Bullet::draw() const {
if (is_active_ && shape_) { if (is_active_ && shape_) {
// Les bales roten segons l'angle de trayectòria (estático tras disparo) Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, config_->colors.normal);
Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, Defaults::Palette::BULLET);
} }
} }
+12 -10
View File
@@ -6,10 +6,12 @@
#include <cstdint> #include <cstdint>
#include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
// Forward declaration — la definició completa s'inclou només al .cpp.
struct BulletConfig;
class Bullet : public Entities::Entity { class Bullet : public Entities::Entity {
public: public:
Bullet() Bullet()
@@ -25,25 +27,25 @@ class Bullet : public Entities::Entity {
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return is_active_; } [[nodiscard]] auto isActive() const -> bool override { return is_active_; }
// Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check) // Override: Interfaz de colisión (radi derivat al ctor des del shape).
[[nodiscard]] auto getCollisionRadius() const -> float override { [[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
return Defaults::Entities::BULLET_RADIUS;
}
[[nodiscard]] auto isCollidable() const -> bool override { [[nodiscard]] auto isCollidable() const -> bool override {
return is_active_; return is_active_;
} }
// Configuració associada (vàlida des del ctor — la bala l'agafa del BulletRegistry).
[[nodiscard]] auto getConfig() const -> const BulletConfig& { return *config_; }
// Getters (API pública sin cambios) // Getters (API pública sin cambios)
[[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; } [[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; }
// Posició al final del frame anterior, per a CCD segment-vs-cercle.
[[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; } [[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; }
void desactivar(); void desactivar();
private: private:
// Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_). const BulletConfig* config_{nullptr}; // apunta al BulletRegistry; vàlid post-ctor
// Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer float collision_radius_{0.0F}; // derivat: shape.bounding_radius × scale × collision_factor
// dejen el objeto en estado coherente (proyectil inactivo, sin owner).
bool is_active_{false}; bool is_active_{false};
uint8_t owner_id_{0}; // 0=P1, 1=P2 uint8_t owner_id_{0}; // 0=P1, 1=P2
Vec2 prev_position_{}; // Posició al final del frame anterior (per a swept collision) Vec2 prev_position_{}; // posició al final del frame anterior (swept CCD)
}; };
+80
View File
@@ -0,0 +1,80 @@
// bullet_config.cpp - Implementació del parser de BulletConfig
// © 2026 JailDesigner
#include "game/entities/bullet_config.hpp"
#include <cstdint>
#include <exception>
#include <iostream>
#include <string>
namespace {
auto parseColor(const fkyaml::node& node, SDL_Color& out) -> bool {
if (!node.is_sequence() || node.size() != 3) {
return false;
}
const auto R = node[0].get_value<uint32_t>();
const auto G = node[1].get_value<uint32_t>();
const auto B = node[2].get_value<uint32_t>();
out = SDL_Color{
.r = static_cast<uint8_t>(R),
.g = static_cast<uint8_t>(G),
.b = static_cast<uint8_t>(B),
.a = 255};
return true;
}
auto parseShape(const fkyaml::node& node, BulletConfig::ShapeCfg& out) -> bool {
if (!node.contains("shape") || !node["shape"].contains("path")) {
std::cerr << "[BulletConfig] Error: falta 'shape.path'\n";
return false;
}
const auto& s = node["shape"];
out.path = s["path"].get_value<std::string>();
out.scale = s.contains("scale") ? s["scale"].get_value<float>() : 1.0F;
out.collision_factor = s.contains("collision_factor")
? s["collision_factor"].get_value<float>()
: 1.0F;
return true;
}
auto parsePhysics(const fkyaml::node& node, BulletConfig::PhysicsCfg& out) -> bool {
if (!node.contains("physics")) {
std::cerr << "[BulletConfig] Error: falta 'physics'\n";
return false;
}
const auto& p = node["physics"];
out.mass = p["mass"].get_value<float>();
out.restitution = p["restitution"].get_value<float>();
out.linear_damping = p["linear_damping"].get_value<float>();
out.angular_damping = p["angular_damping"].get_value<float>();
out.impact_momentum_factor = p["impact_momentum_factor"].get_value<float>();
return true;
}
auto parseColors(const fkyaml::node& node, BulletConfig::ColorsCfg& out) -> bool {
if (!node.contains("colors") || !parseColor(node["colors"]["normal"], out.normal)) {
std::cerr << "[BulletConfig] Error: 'colors.normal' no és [r,g,b]\n";
return false;
}
return true;
}
} // namespace
auto BulletConfig::fromYaml(const fkyaml::node& node) -> std::optional<BulletConfig> {
try {
BulletConfig cfg;
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "bullet";
if (!parseShape(node, cfg.shape)) { return std::nullopt; }
if (!parsePhysics(node, cfg.physics)) { return std::nullopt; }
if (!parseColors(node, cfg.colors)) { return std::nullopt; }
return cfg;
} catch (const std::exception& e) {
std::cerr << "[BulletConfig] Excepció parsejant: " << e.what() << '\n';
return std::nullopt;
}
}
+41
View File
@@ -0,0 +1,41 @@
// bullet_config.hpp - Configuració de la bala carregada des de YAML
// © 2026 JailDesigner
//
// Paral·lel a Player/EnemyConfig. Una sola instància a tot el joc (per ara);
// es comparteix entre totes les bales actives via BulletRegistry.
#pragma once
#include <SDL3/SDL.h>
#include <optional>
#include <string>
#include "external/fkyaml_node.hpp"
struct BulletConfig {
struct ShapeCfg {
std::string path;
float scale;
float collision_factor;
};
struct PhysicsCfg {
float mass;
float restitution;
float linear_damping;
float angular_damping;
float impact_momentum_factor; // factor de transferència bullet→enemic
};
struct ColorsCfg {
SDL_Color normal;
};
std::string name;
ShapeCfg shape;
PhysicsCfg physics;
ColorsCfg colors;
static auto fromYaml(const fkyaml::node& node) -> std::optional<BulletConfig>;
};
+37
View File
@@ -0,0 +1,37 @@
// bullet_registry.cpp - Implementació del registre de bala
// © 2026 JailDesigner
#include "game/entities/bullet_registry.hpp"
#include <cstdlib>
#include <iostream>
#include "core/entities/entity_loader.hpp"
BulletConfig BulletRegistry::config;
bool BulletRegistry::loaded = false;
auto BulletRegistry::load() -> bool {
auto yaml = Entities::EntityLoader::load("bullet");
if (!yaml) {
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar bullet.yaml\n";
return false;
}
auto cfg = BulletConfig::fromYaml(*yaml);
if (!cfg) {
std::cerr << "[BulletRegistry] Error: format invàlid a bullet.yaml\n";
return false;
}
config = *cfg;
loaded = true;
std::cout << "[BulletRegistry] Configuració de bala carregada.\n";
return true;
}
auto BulletRegistry::get() -> const BulletConfig& {
if (!loaded) {
std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n";
std::exit(EXIT_FAILURE);
}
return config;
}
+26
View File
@@ -0,0 +1,26 @@
// bullet_registry.hpp - Registre estàtic de la configuració de la bala
// © 2026 JailDesigner
//
// Una única instància per a tota la sessió. Es manté el patró registry
// (paral·lel a EnemyRegistry) tot i ser una sola entitat: si el dia de demà
// hi ha més tipus de bala (laser/plasma/etc.) només cal estendre-ho.
#pragma once
#include "game/entities/bullet_config.hpp"
class BulletRegistry {
public:
BulletRegistry() = delete;
// Carrega data/entities/bullet/bullet.yaml. Retorna false si falla.
static auto load() -> bool;
// Accés a la configuració. Avorta amb log fatal si load() no s'ha cridat
// o ha fallat.
static auto get() -> const BulletConfig&;
private:
static BulletConfig config;
static bool loaded;
};
+58 -77
View File
@@ -36,18 +36,25 @@ namespace {
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F); return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F);
} }
// Random float [0..1).
auto randFloat01() -> float {
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
}
// Random float [min..max).
auto randRange(float min, float max) -> float {
return min + (randFloat01() * (max - min));
}
} // namespace } // namespace
Enemy::Enemy(Rendering::Renderer* renderer) Enemy::Enemy(Rendering::Renderer* renderer)
: Entity(renderer) { : Entity(renderer) {
brightness_ = Defaults::Brightness::ENEMIC; brightness_ = Defaults::Brightness::ENEMIC;
// Cuerpo físico — defaults comuns; init() ajusta mass/radius segons el tipus. // Body queda amb defaults inocus (radius=0 = no col·lisiona) fins
body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS); // que init() apliqui la configuració del tipus carregada via Registry.
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo) body_.radius = 0.0F;
body_.restitution = Defaults::Enemies::Body::RESTITUTION;
body_.linear_damping = Defaults::Enemies::Body::LINEAR_DAMPING;
body_.angular_damping = Defaults::Enemies::Body::ANGULAR_DAMPING;
} }
void Enemy::init(EnemyType type, const Vec2* ship_pos) { void Enemy::init(EnemyType type, const Vec2* ship_pos) {
@@ -55,7 +62,6 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
config_ = &EnemyRegistry::get(type); config_ = &EnemyRegistry::get(type);
const EnemyConfig& cfg = *config_; const EnemyConfig& cfg = *config_;
// Cas Square: resetejar tracking timer al spawn.
if (type_ == EnemyType::SQUARE) { if (type_ == EnemyType::SQUARE) {
tracking_timer_ = 0.0F; tracking_timer_ = 0.0F;
tracking_strength_ = cfg.behavior.tracking_strength; tracking_strength_ = cfg.behavior.tracking_strength;
@@ -66,14 +72,17 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n'; std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n';
} }
// Radi de col·lisió derivat del cercle circumscrit de la shape * scale * collision_factor. // Radi de col·lisió derivat del cercle circumscrit de la shape.
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F; const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
collision_radius_ = BOUNDING * cfg.shape.scale * cfg.shape.collision_factor; collision_radius_ = BOUNDING * cfg.shape.scale * cfg.shape.collision_factor;
body_.setMass(cfg.physics.mass); body_.setMass(cfg.physics.mass);
body_.radius = collision_radius_; body_.radius = collision_radius_;
body_.restitution = cfg.physics.restitution;
body_.linear_damping = cfg.physics.linear_damping;
body_.angular_damping = cfg.physics.angular_damping;
// Posición aleatoria con comprobación de seguridad // Posició aleatòria amb comprovació de safety_distance.
float min_x; float min_x;
float max_x; float max_x;
float min_y; float min_y;
@@ -85,7 +94,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) { for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) {
float candidate_x; float candidate_x;
float candidate_y; float candidate_y;
if (attemptSafeSpawn(*ship_pos, collision_radius_, candidate_x, candidate_y)) { if (attemptSafeSpawn(*ship_pos, collision_radius_, cfg.spawn.safety_distance, candidate_x, candidate_y)) {
center_.x = candidate_x; center_.x = candidate_x;
center_.y = candidate_y; center_.y = candidate_y;
found_safe_position = true; found_safe_position = true;
@@ -107,8 +116,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
center_.y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y)); center_.y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y));
} }
// Dirección inicial aleatoria, velocidad escalar según tipo const float ANGLE_INICIAL = static_cast<float>(std::rand() % 360) * Constants::PI / 180.0F;
const float ANGLE_INICIAL = (std::rand() % 360) * Constants::PI / 180.0F;
setVelocityFromAngle(ANGLE_INICIAL, cfg.physics.speed); setVelocityFromAngle(ANGLE_INICIAL, cfg.physics.speed);
body_.position = center_; body_.position = center_;
@@ -116,10 +124,8 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.clearAccumulators(); body_.clearAccumulators();
// Rotación visual aleatoria dins del rang del tipus // Rotació visual aleatòria dins del rang del tipus
const float ROTATION_RANGE = cfg.physics.rotation_delta_max - cfg.physics.rotation_delta_min; rotation_delta_ = randRange(cfg.physics.rotation_delta_min, cfg.physics.rotation_delta_max);
rotation_delta_ = cfg.physics.rotation_delta_min +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_RANGE);
rotation_ = 0.0F; rotation_ = 0.0F;
animation_ = EnemyAnimation(); animation_ = EnemyAnimation();
@@ -127,8 +133,8 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
animation_.rotation_delta_target = rotation_delta_; animation_.rotation_delta_target = rotation_delta_;
animation_.rotation_delta_t = 1.0F; animation_.rotation_delta_t = 1.0F;
invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; invulnerability_timer_ = cfg.spawn.invulnerability_duration;
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; brightness_ = cfg.spawn.invulnerability_brightness_start;
direction_change_timer_ = 0.0F; direction_change_timer_ = 0.0F;
@@ -153,17 +159,20 @@ void Enemy::update(float delta_time) {
invulnerability_timer_ -= delta_time; invulnerability_timer_ -= delta_time;
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F); invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; const float T_INV = invulnerability_timer_ / config_->spawn.invulnerability_duration;
const float T = 1.0F - T_INV; const float T = 1.0F - T_INV;
const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; const float START = config_->spawn.invulnerability_brightness_start;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_END; const float END = config_->spawn.invulnerability_brightness_end;
brightness_ = START + ((END - START) * SMOOTH_T); brightness_ = START + ((END - START) * SMOOTH_T);
} }
if (!isWounded()) { if (!isWounded()) {
switch (type_) { switch (type_) {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
case EnemyType::STAR:
// STAR reusa el zigzag esquivador de Pentagon. Si en el futur
// vol comportament propi, separa-li el cas.
behaviorPentagon(delta_time); behaviorPentagon(delta_time);
break; break;
case EnemyType::SQUARE: case EnemyType::SQUARE:
@@ -190,13 +199,11 @@ void Enemy::draw() const {
if (!is_active_ || !shape_) { if (!is_active_ || !shape_) {
return; return;
} }
// El SCALE final = escala base del YAML * modulador dinàmic (spawn/pulse).
const float SCALE = config_->shape.scale * computeCurrentScale(); const float SCALE = config_->shape.scale * computeCurrentScale();
SDL_Color color = config_->colors.normal; SDL_Color color = config_->colors.normal;
// Parpadeo dorado mientras está herido.
if (wounded_timer_ > 0.0F) { if (wounded_timer_ > 0.0F) {
const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ; const float CYCLE = 1.0F / config_->wounded.blink_hz;
const float T = std::fmod(wounded_timer_, CYCLE); const float T = std::fmod(wounded_timer_, CYCLE);
if (T < (CYCLE / 2.0F)) { if (T < (CYCLE / 2.0F)) {
color = config_->colors.wounded; color = config_->colors.wounded;
@@ -217,7 +224,7 @@ void Enemy::destroy() {
} }
void Enemy::hurt(uint8_t shooter_id) { void Enemy::hurt(uint8_t shooter_id) {
wounded_timer_ = Defaults::Enemies::Wounded::DURATION; wounded_timer_ = config_->wounded.duration;
last_hit_by_ = shooter_id; last_hit_by_ = shooter_id;
} }
@@ -230,7 +237,7 @@ void Enemy::setVelocity(float speed) {
if (CURRENT_SPEED > 0.0F) { if (CURRENT_SPEED > 0.0F) {
body_.velocity = body_.velocity * (speed / CURRENT_SPEED); body_.velocity = body_.velocity * (speed / CURRENT_SPEED);
} else { } else {
const float A = (std::rand() % 360) * Constants::PI / 180.0F; const float A = static_cast<float>(std::rand() % 360) * Constants::PI / 180.0F;
setVelocityFromAngle(A, speed); setVelocityFromAngle(A, speed);
} }
} }
@@ -239,17 +246,14 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
body_.velocity = angleToDirection(angle_movement) * speed; body_.velocity = angleToDirection(angle_movement) * speed;
} }
// PENTAGON: zigzag esquivador. Cambios de dirección periódicos (probabilísticos) // PENTAGON: zigzag esquivador. Canvis de direcció periòdics (probabilístics)
// en lugar de detectar paredes; el rebote contra muros lo hace PhysicsWorld // en lloc de detectar parets; el rebot contra murs el fa PhysicsWorld.
// con restitution=1.0.
void Enemy::behaviorPentagon(float delta_time) { void Enemy::behaviorPentagon(float delta_time) {
direction_change_timer_ += delta_time; direction_change_timer_ += delta_time;
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX); if (randFloat01() < config_->behavior.zigzag_prob_per_second * delta_time) {
if (RAND_VAL < config_->behavior.zigzag_prob_per_second * delta_time) {
const float CURRENT_ANGLE = velocityToAngle(body_.velocity); const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * const float DELTA = randFloat01() * config_->behavior.angle_change_max;
config_->behavior.angle_change_max;
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA); const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
const float SPEED = body_.velocity.length(); const float SPEED = body_.velocity.length();
setVelocityFromAngle(NEW_ANGLE, SPEED); setVelocityFromAngle(NEW_ANGLE, SPEED);
@@ -257,8 +261,7 @@ void Enemy::behaviorPentagon(float delta_time) {
} }
} }
// SQUARE: tracking discreto cada tracking_interval. Ajusta dirección // SQUARE: tracking discret cap a la nau cada N segons.
// hacia el ship mezclando con tracking_strength_.
void Enemy::behaviorSquare(float delta_time) { void Enemy::behaviorSquare(float delta_time) {
tracking_timer_ += delta_time; tracking_timer_ += delta_time;
@@ -283,7 +286,7 @@ void Enemy::behaviorSquare(float delta_time) {
} }
} }
// PINWHEEL: movimiento recto + boost de rotación visual cerca del ship. // PINWHEEL: movement rectilini + boost de rotació visual prop del ship.
void Enemy::behaviorPinwheel(float /*delta_time*/) { void Enemy::behaviorPinwheel(float /*delta_time*/) {
if (ship_position_ != nullptr) { if (ship_position_ != nullptr) {
const Vec2 TO_SHIP = *ship_position_ - center_; const Vec2 TO_SHIP = *ship_position_ - center_;
@@ -302,38 +305,26 @@ void Enemy::updateAnimation(float delta_time) {
} }
void Enemy::updatePulse(float delta_time) { void Enemy::updatePulse(float delta_time) {
const auto& cfg = config_->animation.pulse;
if (animation_.pulse_active) { if (animation_.pulse_active) {
animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time; animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time;
animation_.pulse_time_remaining -= delta_time; animation_.pulse_time_remaining -= delta_time;
if (animation_.pulse_time_remaining <= 0.0F) { if (animation_.pulse_time_remaining <= 0.0F) {
animation_.pulse_active = false; animation_.pulse_active = false;
} }
} else { return;
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX); }
const float TRIGGER_PROB = Defaults::Enemies::Animation::PULSE_TRIGGER_PROB * delta_time; if (randFloat01() < cfg.trigger_prob_per_second * delta_time) {
if (RAND_VAL < TRIGGER_PROB) {
animation_.pulse_active = true; animation_.pulse_active = true;
animation_.pulse_phase = 0.0F; animation_.pulse_phase = 0.0F;
animation_.pulse_frequency = randRange(cfg.frequency_min, cfg.frequency_max);
const float FREQ_RANGE = Defaults::Enemies::Animation::PULSE_FREQ_MAX - animation_.pulse_amplitude = randRange(cfg.amplitude_min, cfg.amplitude_max);
Defaults::Enemies::Animation::PULSE_FREQ_MIN; animation_.pulse_time_remaining = randRange(cfg.duration_min, cfg.duration_max);
animation_.pulse_frequency = Defaults::Enemies::Animation::PULSE_FREQ_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * FREQ_RANGE);
const float AMP_RANGE = Defaults::Enemies::Animation::PULSE_AMPLITUD_MAX -
Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN;
animation_.pulse_amplitude = Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * AMP_RANGE);
const float DUR_RANGE = Defaults::Enemies::Animation::PULSE_DURATION_MAX -
Defaults::Enemies::Animation::PULSE_DURATION_MIN;
animation_.pulse_time_remaining = Defaults::Enemies::Animation::PULSE_DURATION_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
}
} }
} }
void Enemy::updateRotationAcceleration(float delta_time) { void Enemy::updateRotationAcceleration(float delta_time) {
const auto& cfg = config_->animation.rotation_accel;
if (animation_.rotation_delta_t < 1.0F) { if (animation_.rotation_delta_t < 1.0F) {
animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration; animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration;
if (animation_.rotation_delta_t >= 1.0F) { if (animation_.rotation_delta_t >= 1.0F) {
@@ -347,34 +338,24 @@ void Enemy::updateRotationAcceleration(float delta_time) {
const float TARGET = animation_.rotation_delta_target; const float TARGET = animation_.rotation_delta_target;
rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T); rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
} }
} else { return;
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTATION_ACCEL_TRIGGER_PROB * delta_time;
if (RAND_VAL < TRIGGER_PROB) {
animation_.rotation_delta_t = 0.0F;
const float MULT_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MAX -
Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN;
const float MULTIPLIER = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * MULT_RANGE);
animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER;
const float DUR_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MAX -
Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN;
animation_.rotation_delta_duration = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
} }
if (randFloat01() < cfg.trigger_prob_per_second * delta_time) {
animation_.rotation_delta_t = 0.0F;
const float MULTIPLIER = randRange(cfg.multiplier_min, cfg.multiplier_max);
animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER;
animation_.rotation_delta_duration = randRange(cfg.duration_min, cfg.duration_max);
} }
} }
auto Enemy::computeCurrentScale() const -> float { auto Enemy::computeCurrentScale() const -> float {
float scale = 1.0F; float scale = 1.0F;
if (invulnerability_timer_ > 0.0F) { if (invulnerability_timer_ > 0.0F) {
const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; const float T_INV = invulnerability_timer_ / config_->spawn.invulnerability_duration;
const float T = 1.0F - T_INV; const float T = 1.0F - T_INV;
const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START; const float START = config_->spawn.invulnerability_scale_start;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END; const float END = config_->spawn.invulnerability_scale_end;
scale = START + ((END - START) * SMOOTH_T); scale = START + ((END - START) * SMOOTH_T);
} else if (animation_.pulse_active) { } else if (animation_.pulse_active) {
scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase); scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase);
@@ -396,7 +377,7 @@ void Enemy::setTrackingStrength(float strength) {
} }
} }
auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float& out_x, float& out_y) -> bool { auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool {
float min_x; float min_x;
float max_x; float max_x;
float min_y; float min_y;
@@ -411,5 +392,5 @@ auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float
const float DX = out_x - ship_pos.x; const float DX = out_x - ship_pos.x;
const float DY = out_y - ship_pos.y; const float DY = out_y - ship_pos.y;
const float DISTANCE = std::sqrt((DX * DX) + (DY * DY)); const float DISTANCE = std::sqrt((DX * DX) + (DY * DY));
return DISTANCE >= Defaults::Enemies::Spawn::SAFETY_DISTANCE; return DISTANCE >= safety_distance;
} }
+4 -4
View File
@@ -6,7 +6,6 @@
#include <cstdint> #include <cstdint>
#include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
@@ -14,7 +13,8 @@
enum class EnemyType : uint8_t { 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)
}; };
// 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.
@@ -142,8 +142,8 @@ class Enemy : public Entities::Entity {
void behaviorSquare(float delta_time); void behaviorSquare(float delta_time);
void behaviorPinwheel(float delta_time); void behaviorPinwheel(float delta_time);
[[nodiscard]] auto computeCurrentScale() const -> float; [[nodiscard]] auto computeCurrentScale() const -> float;
// Static: passa collision_radius com a param per no acoblar a *this. // Static: passa els paràmetres com a args per no acoblar a *this.
static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float& out_x, float& out_y) -> bool; static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool;
// Helper: setear body_.velocity desde un ángulo y magnitud. // Helper: setear body_.velocity desde un ángulo y magnitud.
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL). // angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
+132
View File
@@ -29,6 +29,7 @@ namespace {
if (s == "pentagon") { return EnemyType::PENTAGON; } if (s == "pentagon") { return EnemyType::PENTAGON; }
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; }
return std::nullopt; return std::nullopt;
} }
@@ -80,6 +81,60 @@ namespace {
out.speed = p["speed"].get_value<float>(); out.speed = p["speed"].get_value<float>();
out.rotation_delta_min = p["rotation_delta_min"].get_value<float>(); out.rotation_delta_min = p["rotation_delta_min"].get_value<float>();
out.rotation_delta_max = p["rotation_delta_max"].get_value<float>(); out.rotation_delta_max = p["rotation_delta_max"].get_value<float>();
out.restitution = p["restitution"].get_value<float>();
out.linear_damping = p["linear_damping"].get_value<float>();
out.angular_damping = p["angular_damping"].get_value<float>();
return true;
}
auto parseAnimation(const fkyaml::node& node, const std::string& name, EnemyConfig::AnimationCfg& out) -> bool {
if (!node.contains("animation") ||
!node["animation"].contains("pulse") ||
!node["animation"].contains("rotation_accel")) {
std::cerr << "[EnemyConfig] Error: falta 'animation.pulse' o 'animation.rotation_accel' a " << name << '\n';
return false;
}
const auto& p = node["animation"]["pulse"];
out.pulse.trigger_prob_per_second = p["trigger_prob_per_second"].get_value<float>();
out.pulse.duration_min = p["duration_min"].get_value<float>();
out.pulse.duration_max = p["duration_max"].get_value<float>();
out.pulse.amplitude_min = p["amplitude_min"].get_value<float>();
out.pulse.amplitude_max = p["amplitude_max"].get_value<float>();
out.pulse.frequency_min = p["frequency_min"].get_value<float>();
out.pulse.frequency_max = p["frequency_max"].get_value<float>();
const auto& r = node["animation"]["rotation_accel"];
out.rotation_accel.trigger_prob_per_second = r["trigger_prob_per_second"].get_value<float>();
out.rotation_accel.duration_min = r["duration_min"].get_value<float>();
out.rotation_accel.duration_max = r["duration_max"].get_value<float>();
out.rotation_accel.multiplier_min = r["multiplier_min"].get_value<float>();
out.rotation_accel.multiplier_max = r["multiplier_max"].get_value<float>();
return true;
}
auto parseWounded(const fkyaml::node& node, const std::string& name, EnemyConfig::WoundedCfg& out) -> bool {
if (!node.contains("wounded")) {
std::cerr << "[EnemyConfig] Error: falta 'wounded' a " << name << '\n';
return false;
}
const auto& w = node["wounded"];
out.duration = w["duration"].get_value<float>();
out.blink_hz = w["blink_hz"].get_value<float>();
return true;
}
auto parseSpawn(const fkyaml::node& node, const std::string& name, EnemyConfig::SpawnCfg& out) -> bool {
if (!node.contains("spawn")) {
std::cerr << "[EnemyConfig] Error: falta 'spawn' a " << name << '\n';
return false;
}
const auto& s = node["spawn"];
out.invulnerability_duration = s["invulnerability_duration"].get_value<float>();
out.invulnerability_brightness_start = s["invulnerability_brightness_start"].get_value<float>();
out.invulnerability_brightness_end = s["invulnerability_brightness_end"].get_value<float>();
out.invulnerability_scale_start = s["invulnerability_scale_start"].get_value<float>();
out.invulnerability_scale_end = s["invulnerability_scale_end"].get_value<float>();
out.safety_distance = s["safety_distance"].get_value<float>();
return true; return true;
} }
@@ -122,6 +177,79 @@ namespace {
return true; return true;
} }
auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> {
if (s == "set_hurt") { return EnemyActionType::SET_HURT; }
if (s == "destroy") { return EnemyActionType::DESTROY; }
if (s == "add_score") { return EnemyActionType::ADD_SCORE; }
if (s == "create_debris") { return EnemyActionType::CREATE_DEBRIS; }
if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; }
if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; }
return std::nullopt;
}
auto parseActionList(const fkyaml::node& list_node, const std::string& enemy_name, const char* event_name, std::vector<EnemyAction>& out) -> bool {
if (!list_node.is_sequence()) {
std::cerr << "[EnemyConfig] Error: '" << event_name << "' ha de ser una llista a "
<< enemy_name << '\n';
return false;
}
for (const auto& item : list_node) {
if (!item.contains("action")) {
std::cerr << "[EnemyConfig] Error: entrada sense 'action' a " << event_name
<< " (" << enemy_name << ")\n";
return false;
}
const auto STR = item["action"].get_value<std::string>();
const auto PARSED = actionTypeFromString(STR);
if (!PARSED) {
std::cerr << "[EnemyConfig] Error: acció desconeguda '" << STR << "' a "
<< event_name << " (" << enemy_name << ")\n";
return false;
}
out.push_back({*PARSED});
}
return true;
}
// Defaults: replica el flux hardcoded actual (set_hurt → destroy → score+debris+fireworks).
void fillLegacyDefaults(EnemyEventConfig& events) {
events.on_hit = {{EnemyActionType::SET_HURT}};
events.on_hurt_end = {{EnemyActionType::DESTROY}};
events.on_destroy = {
{EnemyActionType::ADD_SCORE},
{EnemyActionType::CREATE_DEBRIS},
{EnemyActionType::CREATE_FIREWORKS},
};
}
auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
if (!node.contains("events")) {
fillLegacyDefaults(out);
return true;
}
const auto& e = node["events"];
if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) {
return false;
}
if (e.contains("on_hurt_end") &&
!parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) {
return false;
}
if (e.contains("on_destroy") &&
!parseActionList(e["on_destroy"], name, "on_destroy", out.on_destroy)) {
return false;
}
// Validació: destroy no pot aparèixer dins on_destroy (recursió infinita).
for (const auto& a : out.on_destroy) {
if (a.type == EnemyActionType::DESTROY) {
std::cerr << "[EnemyConfig] Error: 'destroy' no pot aparèixer dins 'on_destroy' a "
<< name << " (recursió infinita)\n";
return false;
}
}
return true;
}
} // namespace } // namespace
auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type) auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
@@ -134,8 +262,12 @@ auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
if (!parseShape(node, cfg.name, cfg.shape)) { return std::nullopt; } if (!parseShape(node, cfg.name, cfg.shape)) { return std::nullopt; }
if (!parsePhysics(node, cfg.name, cfg.physics)) { return std::nullopt; } if (!parsePhysics(node, cfg.name, cfg.physics)) { return std::nullopt; }
parseBehavior(node, cfg.behavior); parseBehavior(node, cfg.behavior);
if (!parseAnimation(node, cfg.name, cfg.animation)) { return std::nullopt; }
if (!parseWounded(node, cfg.name, cfg.wounded)) { 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; }
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
return cfg; return cfg;
} catch (const std::exception& e) { } catch (const std::exception& e) {
+45
View File
@@ -14,6 +14,7 @@
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "game/entities/enemy.hpp" // EnemyType #include "game/entities/enemy.hpp" // EnemyType
#include "game/entities/enemy_event.hpp"
struct EnemyConfig { struct EnemyConfig {
struct ShapeCfg { struct ShapeCfg {
@@ -27,6 +28,9 @@ struct EnemyConfig {
float speed; float speed;
float rotation_delta_min; float rotation_delta_min;
float rotation_delta_max; float rotation_delta_max;
float restitution; // rebot contra parets (1.0 = perfectament elàstic)
float linear_damping; // fricció lineal (s^-1)
float angular_damping;
}; };
// Camps específics de cada AI. Els no aplicables a un tipus queden a 0.0F // Camps específics de cada AI. Els no aplicables a un tipus queden a 0.0F
@@ -43,6 +47,43 @@ struct EnemyConfig {
float proximity_distance{0.0F}; float proximity_distance{0.0F};
}; };
// Animacions decoratives. Compartides estructuralment entre tots els tipus
// però amb valors propis per personalitzar la "personalitat" visual de cada un.
struct AnimationCfg {
struct PulseCfg {
float trigger_prob_per_second; // probabilitat per segon d'iniciar un pulse
float duration_min;
float duration_max;
float amplitude_min; // amplitud d'escala (±)
float amplitude_max;
float frequency_min; // Hz
float frequency_max;
};
struct RotationAccelCfg {
float trigger_prob_per_second;
float duration_min; // segons de transició al nou speed
float duration_max;
float multiplier_min; // multiplicador sobre rotation_delta_base
float multiplier_max;
};
PulseCfg pulse;
RotationAccelCfg rotation_accel;
};
struct WoundedCfg {
float duration; // segons en estat ferit abans d'explotar
float blink_hz; // freqüència del parpelleig color normal ↔ wounded
};
struct SpawnCfg {
float invulnerability_duration; // segons d'invulnerabilitat post-spawn
float invulnerability_brightness_start; // brightness inicial (corba LERP)
float invulnerability_brightness_end; // brightness final
float invulnerability_scale_start; // escala inicial (corba LERP, 0 = invisible)
float invulnerability_scale_end; // escala final (1 = mida nativa)
float safety_distance; // px mínim respecte al player al spawn
};
struct ColorsCfg { struct ColorsCfg {
SDL_Color normal; SDL_Color normal;
SDL_Color wounded; SDL_Color wounded;
@@ -53,8 +94,12 @@ struct EnemyConfig {
ShapeCfg shape; ShapeCfg shape;
PhysicsCfg physics; PhysicsCfg physics;
BehaviorCfg behavior; BehaviorCfg behavior;
AnimationCfg animation;
WoundedCfg wounded;
SpawnCfg spawn;
ColorsCfg colors; ColorsCfg colors;
int score; int score;
EnemyEventConfig events;
// Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del // Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del
// YAML coincideix amb el tipus que el caller espera (segons el directori). // YAML coincideix amb el tipus que el caller espera (segons el directori).
+48
View File
@@ -0,0 +1,48 @@
// enemy_event.hpp - Sistema declaratiu d'events i accions per a enemics
// © 2026 JailDesigner
//
// Cada enemic descriu al seu YAML què passa quan rep un event (on_hit,
// on_hurt_end, on_destroy) com a llista d'accions. El motor només dispatcha;
// el comportament viu a les dades.
#pragma once
#include <cstdint>
#include <vector>
enum class EnemyEventType : uint8_t {
ON_HIT, // Impactat per una bala
ON_HURT_END, // Timer wounded ha expirat aquest frame
ON_DESTROY, // L'acció destroy s'està executant (efectes col·laterals)
};
enum class EnemyActionType : uint8_t {
SET_HURT, // Entra estat wounded (o destrueix si ja era wounded)
DESTROY, // Dispara on_destroy + desactiva físicament
ADD_SCORE, // Suma config.score al shooter + floating score
CREATE_DEBRIS, // Explosió de debris amb herència de velocitat
CREATE_FIREWORKS, // Burst radial de firework
APPLY_IMPULSE, // Aplica l'impuls de la bala impactant
};
struct EnemyAction {
EnemyActionType type;
};
struct EnemyEventConfig {
std::vector<EnemyAction> on_hit;
std::vector<EnemyAction> on_hurt_end;
std::vector<EnemyAction> on_destroy;
[[nodiscard]] auto getActions(EnemyEventType event) const -> const std::vector<EnemyAction>& {
switch (event) {
case EnemyEventType::ON_HIT:
return on_hit;
case EnemyEventType::ON_HURT_END:
return on_hurt_end;
case EnemyEventType::ON_DESTROY:
return on_destroy;
}
return on_hit; // unreachable
}
};
+6 -2
View File
@@ -12,6 +12,7 @@
EnemyConfig EnemyRegistry::pentagon_config; EnemyConfig EnemyRegistry::pentagon_config;
EnemyConfig EnemyRegistry::square_config; EnemyConfig EnemyRegistry::square_config;
EnemyConfig EnemyRegistry::pinwheel_config; EnemyConfig EnemyRegistry::pinwheel_config;
EnemyConfig EnemyRegistry::star_config;
bool EnemyRegistry::loaded = false; bool EnemyRegistry::loaded = false;
namespace { namespace {
@@ -36,10 +37,11 @@ namespace {
auto EnemyRegistry::loadAll() -> bool { 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);
loaded = OK; loaded = OK;
if (OK) { if (OK) {
std::cout << "[EnemyRegistry] 3 configuracions d'enemic carregades.\n"; std::cout << "[EnemyRegistry] 4 configuracions d'enemic carregades.\n";
} }
return OK; return OK;
} }
@@ -56,6 +58,8 @@ auto EnemyRegistry::get(EnemyType type) -> const EnemyConfig& {
return square_config; return square_config;
case EnemyType::PINWHEEL: case EnemyType::PINWHEEL:
return pinwheel_config; return pinwheel_config;
case EnemyType::STAR:
return star_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
@@ -26,5 +26,6 @@ class EnemyRegistry {
static EnemyConfig pentagon_config; static EnemyConfig pentagon_config;
static EnemyConfig square_config; static EnemyConfig square_config;
static EnemyConfig pinwheel_config; static EnemyConfig pinwheel_config;
static EnemyConfig star_config;
static bool loaded; static bool loaded;
}; };
+8
View File
@@ -15,6 +15,7 @@
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/system/scene_context.hpp" #include "core/system/scene_context.hpp"
#include "core/system/service_menu.hpp" #include "core/system/service_menu.hpp"
#include "game/entities/bullet_registry.hpp"
#include "game/entities/enemy_registry.hpp" #include "game/entities/enemy_registry.hpp"
#include "game/entities/player_config.hpp" #include "game/entities/player_config.hpp"
#include "game/stage_system/stage_loader.hpp" #include "game/stage_system/stage_loader.hpp"
@@ -72,6 +73,13 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
std::exit(EXIT_FAILURE); std::exit(EXIT_FAILURE);
} }
// Carregar la configuració de la bala. Cal abans de construir el pool de
// bullets, ja que cada Bullet llegeix el registry al seu ctor.
if (!BulletRegistry::load()) {
std::cerr << "[GameScene] FATAL: no s'ha pogut carregar bullet.yaml\n";
std::exit(EXIT_FAILURE);
}
// Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual. // Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual.
ships_[0] = Ship(sdl.getRenderer(), *player_config); // Jugador 1: nau estàndard ships_[0] = Ship(sdl.getRenderer(), *player_config); // Jugador 1: nau estàndard
ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship2.shp"); // Jugador 2: interceptor amb ales ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship2.shp"); // Jugador 2: interceptor amb ales
@@ -136,8 +136,11 @@ namespace StageSystem {
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) { if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
return EnemyType::SQUARE; return EnemyType::SQUARE;
} }
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado + config_->distribucio.molinillo) {
return EnemyType::PINWHEEL; return EnemyType::PINWHEEL;
} }
return EnemyType::STAR;
}
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
@@ -28,6 +28,7 @@ namespace StageSystem {
uint8_t pentagon; // 0-100 uint8_t pentagon; // 0-100
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)
// Suma ha de ser 100, validat en StageLoader // Suma ha de ser 100, validat en StageLoader
}; };
@@ -59,7 +60,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 == 100; distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo + distribucio.star == 100;
} }
}; };
+19 -17
View File
@@ -19,7 +19,7 @@
namespace StageSystem { namespace StageSystem {
auto StageLoader::load(const std::string& path) -> std::unique_ptr<StageSystemConfig> { auto StageLoader::load(const std::string& path) -> std::unique_ptr<StageSystemConfig> {
try { try {
// Normalize path: "data/stages/stages.yaml" → "stages/stages.yaml" // Normalize path: "data/stages/stages.yaml" → "stages/stages.yaml"
std::string normalized = path; std::string normalized = path;
@@ -83,9 +83,9 @@ auto StageLoader::load(const std::string& path) -> std::unique_ptr<StageSystemCo
std::cerr << "[StageLoader] Excepció: " << e.what() << '\n'; std::cerr << "[StageLoader] Excepció: " << e.what() << '\n';
return nullptr; return nullptr;
} }
} }
auto StageLoader::parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool { auto StageLoader::parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool {
try { try {
if (!yaml.contains("version") || !yaml.contains("total_stages")) { if (!yaml.contains("version") || !yaml.contains("total_stages")) {
std::cerr << "[StageLoader] Error: metadata incompleta" << '\n'; std::cerr << "[StageLoader] Error: metadata incompleta" << '\n';
@@ -103,9 +103,9 @@ auto StageLoader::parseMetadata(const fkyaml::node& yaml, MetadataStages& meta)
std::cerr << "[StageLoader] Error parsing metadata: " << e.what() << '\n'; std::cerr << "[StageLoader] Error parsing metadata: " << e.what() << '\n';
return false; return false;
} }
} }
auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool { auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool {
try { try {
if (!yaml.contains("stage_id") || !yaml.contains("total_enemies") || if (!yaml.contains("stage_id") || !yaml.contains("total_enemies") ||
!yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") || !yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") ||
@@ -138,9 +138,9 @@ auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bo
std::cerr << "[StageLoader] Error parsing stage: " << e.what() << '\n'; std::cerr << "[StageLoader] Error parsing stage: " << e.what() << '\n';
return false; return false;
} }
} }
auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool { auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool {
try { try {
if (!yaml.contains("mode") || !yaml.contains("initial_delay") || if (!yaml.contains("mode") || !yaml.contains("initial_delay") ||
!yaml.contains("spawn_interval")) { !yaml.contains("spawn_interval")) {
@@ -158,9 +158,9 @@ auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config
std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << '\n'; std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << '\n';
return false; return false;
} }
} }
auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool { auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool {
try { try {
if (!yaml.contains("pentagon") || !yaml.contains("cuadrado") || if (!yaml.contains("pentagon") || !yaml.contains("cuadrado") ||
!yaml.contains("molinillo")) { !yaml.contains("molinillo")) {
@@ -171,9 +171,11 @@ auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics
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).
dist.star = yaml.contains("star") ? yaml["star"].get_value<uint8_t>() : 0;
// Validar que suma 100 // Validar que suma 100
int sum = dist.pentagon + dist.cuadrado + dist.molinillo; int sum = dist.pentagon + dist.cuadrado + dist.molinillo + dist.star;
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;
@@ -184,9 +186,9 @@ auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics
std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << '\n'; std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << '\n';
return false; return false;
} }
} }
auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool { auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool {
try { try {
if (!yaml.contains("speed_multiplier") || !yaml.contains("rotation_multiplier") || if (!yaml.contains("speed_multiplier") || !yaml.contains("rotation_multiplier") ||
!yaml.contains("tracking_strength")) { !yaml.contains("tracking_strength")) {
@@ -214,9 +216,9 @@ auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDific
std::cerr << "[StageLoader] Error parsing multipliers: " << e.what() << '\n'; std::cerr << "[StageLoader] Error parsing multipliers: " << e.what() << '\n';
return false; return false;
} }
} }
auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn { auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn {
if (mode_str == "progressive") { if (mode_str == "progressive") {
return ModeSpawn::PROGRESSIVE; return ModeSpawn::PROGRESSIVE;
} }
@@ -229,9 +231,9 @@ auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn {
std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str
<< "', usant PROGRESSIVE" << '\n'; << "', usant PROGRESSIVE" << '\n';
return ModeSpawn::PROGRESSIVE; return ModeSpawn::PROGRESSIVE;
} }
auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool { auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool {
if (config.stages.empty()) { if (config.stages.empty()) {
std::cerr << "[StageLoader] Error: sin stage carregat" << '\n'; std::cerr << "[StageLoader] Error: sin stage carregat" << '\n';
return false; return false;
@@ -254,6 +256,6 @@ auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool {
} }
return true; return true;
} }
} // namespace StageSystem } // namespace StageSystem
+12 -76
View File
@@ -8,61 +8,12 @@
#include "core/physics/collision.hpp" #include "core/physics/collision.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
#include "game/entities/enemy_config.hpp" #include "game/entities/bullet_config.hpp"
#include "game/systems/enemy_event_dispatcher.hpp"
namespace Systems::Collision { namespace Systems::Collision {
namespace { namespace {
constexpr uint8_t NO_SHOOTER = 0xFF;
// 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 int POINTS = enemy.getConfig().score;
const SDL_Color COLOR = enemy.getConfig().colors.normal;
const SDL_Color WOUNDED_COLOR = enemy.getConfig().colors.wounded;
if (shooter_id != NO_SHOOTER) {
ctx.score_per_player[shooter_id] += POINTS;
}
ctx.floating_score_manager.crear(POINTS, ENEMY_POS);
enemy.destroy();
constexpr float SPEED_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
SPEED_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).
// Línia blanca + halo daurat (WOUNDED) per a feel d'espurnes.
ctx.firework_manager.spawn(ENEMY_POS,
Defaults::FX::Firework::DEFAULT_COLOR,
Defaults::FX::Firework::SPEED,
Defaults::FX::Firework::N_POINTS,
Defaults::FX::Firework::INITIAL_BRIGHTNESS,
/*glow=*/true,
WOUNDED_COLOR);
}
// Trenca una bala en debris (8 fragments de l'octàgon) + so HIT + desactiva. // 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, // S'invoca des de qualsevol desactivació de bala (impacte amb enemic, amb jugador,
@@ -80,7 +31,7 @@ namespace Systems::Collision {
0.0F, // sense velocity angular heretada 0.0F, // sense velocity angular heretada
0.0F, // sense rotació visual heretada 0.0F, // sense rotació visual heretada
Defaults::Sound::HIT, Defaults::Sound::HIT,
Defaults::Palette::BULLET, bullet.getConfig().colors.normal,
Defaults::Physics::Debris::TEMPS_VIDA, Defaults::Physics::Debris::TEMPS_VIDA,
Defaults::Physics::Debris::ACCELERACIO, Defaults::Physics::Debris::ACCELERACIO,
1); // sense duplicat de segments 1); // sense duplicat de segments
@@ -96,31 +47,16 @@ namespace Systems::Collision {
continue; continue;
} }
for (auto& enemy : ctx.enemies) { for (auto& enemy : ctx.enemies) {
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, enemy, AMPLIFIER)) { if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), enemy, AMPLIFIER)) {
continue; continue;
} }
// *** COLISIÓN bullet → enemy *** // *** COLISIÓN bullet → enemy ***
// Empuje físico cuasi-realista: el impulse és el moment de la bala // La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu
// (m·v) multiplicat pel factor de transferència. Direcció = vector // al YAML de l'enemic via la secció `events:`. Aquí només dispatchem.
// velocity de la bala (cap a on viatjava). Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, bullet.getOwnerId(), &bullet);
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.hurt(SHOOTER);
}
breakBullet(ctx.debris_manager, bullet); breakBullet(ctx.debris_manager, bullet);
break; // Una bala impacta a un enemy y muere break;
} }
} }
} }
@@ -131,7 +67,7 @@ namespace Systems::Collision {
continue; continue;
} }
enemy.consumeWoundExpired(); enemy.consumeWoundExpired();
explodeNow(ctx, enemy, enemy.getLastHitBy()); Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HURT_END, enemy.getLastHitBy());
} }
} }
@@ -244,7 +180,7 @@ namespace Systems::Collision {
continue; continue;
} }
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, ctx.ships[player_id], AMPLIFIER)) { if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), ctx.ships[player_id], AMPLIFIER)) {
continue; continue;
} }
@@ -253,7 +189,7 @@ namespace Systems::Collision {
// de la bala a la nau ABANS de on_player_hit perquè tocado() // de la bala a la nau ABANS de on_player_hit perquè tocado()
// captura la velocitat per als debris (si no, queden quiets). // captura la velocitat per als debris (si no, queden quiets).
const Vec2 BULLET_IMPULSE = bullet.getBody().velocity * const Vec2 BULLET_IMPULSE = bullet.getBody().velocity *
(bullet.getBody().mass * Defaults::Physics::Bullet::IMPACT_MOMENTUM_FACTOR); (bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor);
ctx.ships[player_id].getBody().applyImpulse(BULLET_IMPULSE); ctx.ships[player_id].getBody().applyImpulse(BULLET_IMPULSE);
ctx.on_player_hit(player_id); ctx.on_player_hit(player_id);
ctx.lives_per_player[BULLET_OWNER]++; ctx.lives_per_player[BULLET_OWNER]++;
@@ -280,12 +216,12 @@ namespace Systems::Collision {
float min_y; float min_y;
float max_y; float max_y;
Constants::getPlayAreaBounds(min_x, max_x, min_y, max_y); Constants::getPlayAreaBounds(min_x, max_x, min_y, max_y);
constexpr float R = Defaults::Entities::BULLET_RADIUS;
for (auto& bullet : bullets) { for (auto& bullet : bullets) {
if (!bullet.isActive()) { if (!bullet.isActive()) {
continue; continue;
} }
const float R = bullet.getCollisionRadius();
const Vec2& pos = bullet.getCenter(); const Vec2& pos = bullet.getCenter();
if (pos.x < min_x + R || pos.x > max_x - R || if (pos.x < min_x + R || pos.x > max_x - R ||
pos.y < min_y + R || pos.y > max_y - R) { pos.y < min_y + R || pos.y > max_y - R) {
@@ -0,0 +1,101 @@
// enemy_event_dispatcher.cpp - Implementació del dispatcher d'events d'enemic
// © 2026 JailDesigner
#include "game/systems/enemy_event_dispatcher.hpp"
#include <cstdint>
#include "core/defaults.hpp"
#include "core/types.hpp"
#include "game/entities/bullet.hpp"
#include "game/entities/bullet_config.hpp"
#include "game/entities/enemy_config.hpp"
namespace Systems::EnemyEvents {
namespace {
constexpr uint8_t NO_SHOOTER = 0xFF;
void doAddScore(Systems::Collision::Context& ctx, const Enemy& enemy, uint8_t shooter) {
const int POINTS = enemy.getConfig().score;
if (shooter != NO_SHOOTER) {
ctx.score_per_player[shooter] += POINTS;
}
ctx.floating_score_manager.crear(POINTS, enemy.getCenter());
}
void doCreateDebris(Systems::Collision::Context& ctx, const Enemy& enemy) {
constexpr float SPEED_EXPLOSIO = 80.0F;
const Vec2 INHERITED_VEL = enemy.getVelocityVector() *
Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
ctx.debris_manager.explode(
enemy.getShape(),
enemy.getCenter(),
0.0F,
1.0F,
SPEED_EXPLOSIO,
enemy.getBrightness(),
INHERITED_VEL,
0.0F,
0.0F,
Defaults::Sound::EXPLOSION,
enemy.getConfig().colors.normal,
Defaults::Physics::Debris::ENEMY_LIFETIME,
Defaults::Physics::Debris::ENEMY_FRICTION,
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
}
void doCreateFireworks(Systems::Collision::Context& ctx, const Enemy& enemy) {
ctx.firework_manager.spawn(enemy.getCenter(),
Defaults::FX::Firework::DEFAULT_COLOR,
Defaults::FX::Firework::SPEED,
Defaults::FX::Firework::N_POINTS,
Defaults::FX::Firework::INITIAL_BRIGHTNESS,
/*glow=*/true,
enemy.getConfig().colors.wounded);
}
void doApplyImpulse(Enemy& enemy, const Bullet* bullet) {
if (bullet == nullptr) {
return;
}
const Vec2 IMPULSE = bullet->getBody().velocity *
(bullet->getBody().mass * bullet->getConfig().physics.impact_momentum_factor);
enemy.applyImpulse(IMPULSE);
}
} // namespace
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);
for (const auto& action : actions) {
switch (action.type) {
case EnemyActionType::SET_HURT:
if (enemy.isWounded()) {
// Segon hit sobre wounded → mort immediata (regla 2-hits).
dispatchEvent(ctx, enemy, EnemyEventType::ON_DESTROY, shooter_id, bullet);
enemy.destroy();
} else {
enemy.hurt(shooter_id);
}
break;
case EnemyActionType::DESTROY:
dispatchEvent(ctx, enemy, EnemyEventType::ON_DESTROY, shooter_id, bullet);
enemy.destroy();
break;
case EnemyActionType::ADD_SCORE:
doAddScore(ctx, enemy, shooter_id);
break;
case EnemyActionType::CREATE_DEBRIS:
doCreateDebris(ctx, enemy);
break;
case EnemyActionType::CREATE_FIREWORKS:
doCreateFireworks(ctx, enemy);
break;
case EnemyActionType::APPLY_IMPULSE:
doApplyImpulse(enemy, bullet);
break;
}
}
}
} // namespace Systems::EnemyEvents
@@ -0,0 +1,23 @@
// enemy_event_dispatcher.hpp - Executa les accions YAML d'un event d'enemic
// © 2026 JailDesigner
//
// Mira la llista d'EnemyAction associada a l'event al config de l'enemic i les
// executa una per una. L'acció DESTROY dispara recursivament ON_DESTROY abans
// de desactivar físicament l'enemic (el parser garanteix que ON_DESTROY no
// conté DESTROY, evitant recursió infinita).
#pragma once
#include <cstdint>
#include "game/entities/enemy_event.hpp"
#include "game/systems/collision_system.hpp"
namespace Systems::EnemyEvents {
// shooter_id: id del jugador que ha disparat (0xFF = sense atribució).
// bullet: punter opcional a la bala que ha causat l'event (usat per APPLY_IMPULSE);
// nullptr per a events no derivats d'una bala (on_hurt_end).
void dispatchEvent(Systems::Collision::Context& ctx, Enemy& enemy, EnemyEventType event, uint8_t shooter_id, const Bullet* bullet = nullptr);
} // namespace Systems::EnemyEvents