Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c0502eefb | |||
| 9b3da3a6e7 | |||
| bc41169176 | |||
| b3a1afce06 | |||
| 4b6dc8a47a | |||
| 3dadd5fc1a | |||
| bea844d51e | |||
| 5fb6c68df4 | |||
| 866a057704 | |||
| da8eab330d | |||
| 39bda0775e | |||
| ed4d3a3915 | |||
| 6447932212 | |||
| 9f278772bb | |||
| 2d073b6055 |
+4
-1
@@ -110,7 +110,10 @@ add_executable(pack_resources EXCLUDE_FROM_ALL
|
||||
tools/pack_resources/pack_resources.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
)
|
||||
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
||||
target_include_directories(pack_resources PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
||||
|
||||
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,68 @@
|
||||
name: pentagon
|
||||
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
|
||||
|
||||
shape:
|
||||
path: enemy_pentagon.shp
|
||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||
|
||||
physics:
|
||||
mass: 5.0
|
||||
speed: 35.0 # px/s (esquivador lent)
|
||||
rotation_delta_min: 0.75 # rad/s — rotació visual mínima
|
||||
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:
|
||||
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
|
||||
angle_change_max: 1.0 # rad — magnitud del canvi de direcció
|
||||
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:
|
||||
normal: [0, 255, 255] # Cyan pur "esquivador"
|
||||
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
|
||||
|
||||
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
|
||||
@@ -0,0 +1,66 @@
|
||||
name: pinwheel
|
||||
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
|
||||
|
||||
shape:
|
||||
path: enemy_pinwheel.shp
|
||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||
|
||||
physics:
|
||||
mass: 4.0 # Més lleuger — àgil
|
||||
speed: 50.0 # px/s (el més ràpid)
|
||||
rotation_delta_min: 3.0 # rad/s — rotació base elevada
|
||||
rotation_delta_max: 6.0
|
||||
restitution: 1.0
|
||||
linear_damping: 0.0
|
||||
angular_damping: 0.0
|
||||
|
||||
behavior:
|
||||
# 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
|
||||
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:
|
||||
normal: [255, 0, 255] # Magenta pur "agressiu"
|
||||
wounded: [255, 220, 60]
|
||||
|
||||
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
|
||||
@@ -0,0 +1,49 @@
|
||||
name: player_ship
|
||||
|
||||
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
|
||||
# Nota: el segon jugador rep un override del shape ("ship2.shp") al ctor.
|
||||
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
|
||||
# per cada model.
|
||||
#
|
||||
# scale: multiplicador visual i de hitbox sobre la mida nativa del .shp (1.0 = mida del fitxer).
|
||||
# collision_factor: ajust opcional del hitbox respecte el cercle circumscrit
|
||||
# automàtic de la shape; tocar només si el feel del hitbox
|
||||
# no quadra amb la silueta visual (default 1.0).
|
||||
shape:
|
||||
path: ship.shp
|
||||
scale: 1.0
|
||||
collision_factor: 1.0
|
||||
|
||||
physics:
|
||||
mass: 10.0
|
||||
restitution: 0.6
|
||||
linear_damping: 1.5
|
||||
angular_damping: 0.0
|
||||
rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia)
|
||||
acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST
|
||||
max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade)
|
||||
# Factor de transferència del moment lineal de la nau a l'enemic en el
|
||||
# frame exacte que mor per col·lisió (afegit per damunt del rebot natural).
|
||||
death_impact_factor: 0.3
|
||||
|
||||
invulnerability:
|
||||
duration: 3.0 # segons d'invulnerabilitat post-respawn
|
||||
blink_visible: 0.1 # segons visible per cicle de parpelleig
|
||||
blink_invisible: 0.1 # segons invisible per cicle de parpelleig
|
||||
|
||||
hurt:
|
||||
duration: 15.0 # segons en estat "ferit" abans de tornar a normal
|
||||
blink_hz: 10.0 # freqüència parpelleig color normal <-> color hurt
|
||||
|
||||
# Empenta visual: la nau s'escala lleugerament amb la velocitat.
|
||||
# Manté la sensació del Pascal original (0..MAX_VEL → 1.0..~1.5).
|
||||
visual_thrust:
|
||||
push_divisor: 33.33
|
||||
scale_divisor: 12.0
|
||||
|
||||
colors:
|
||||
normal: [255, 255, 255] # blanc neutre
|
||||
hurt: [255, 220, 60] # daurat (estat ferit)
|
||||
|
||||
weapon:
|
||||
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
|
||||
@@ -0,0 +1,66 @@
|
||||
name: square
|
||||
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
|
||||
|
||||
shape:
|
||||
path: enemy_square.shp
|
||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||
|
||||
physics:
|
||||
mass: 8.0 # Més pesat — "tanc"
|
||||
speed: 40.0 # px/s (velocitat mitjana)
|
||||
rotation_delta_min: 0.3 # rad/s — rotació lenta
|
||||
rotation_delta_max: 1.5
|
||||
restitution: 1.0
|
||||
linear_damping: 0.0
|
||||
angular_damping: 0.0
|
||||
|
||||
behavior:
|
||||
# Square: tracking discret cap a la nau cada N segons.
|
||||
tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1)
|
||||
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:
|
||||
normal: [255, 0, 0] # Roig pur "tanc"
|
||||
wounded: [255, 220, 60]
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -7,7 +7,7 @@ metadata:
|
||||
description: "Progressive difficulty curve from novice to expert"
|
||||
|
||||
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
|
||||
total_enemies: 50
|
||||
spawn_config:
|
||||
@@ -15,9 +15,10 @@ stages:
|
||||
initial_delay: 0.3
|
||||
spawn_interval: 0.4
|
||||
enemy_distribution:
|
||||
pentagon: 34
|
||||
cuadrado: 33
|
||||
molinillo: 33
|
||||
pentagon: 25
|
||||
cuadrado: 25
|
||||
molinillo: 25
|
||||
star: 25
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 0.7
|
||||
rotation_multiplier: 0.8
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include "core/defaults/physics.hpp"
|
||||
#include "core/defaults/playfield.hpp"
|
||||
#include "core/defaults/rendering.hpp"
|
||||
#include "core/defaults/ship.hpp"
|
||||
#include "core/defaults/starfield_parallax.hpp"
|
||||
#include "core/defaults/title.hpp"
|
||||
#include "core/defaults/trail.hpp"
|
||||
|
||||
@@ -1,101 +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
|
||||
//
|
||||
// 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
|
||||
|
||||
#include "core/defaults/entities.hpp"
|
||||
namespace Defaults::Enemies::Spawn {
|
||||
|
||||
namespace Defaults::Enemies {
|
||||
// Sostre de reintents al cercar una posició de spawn que respecti el
|
||||
// safety_distance del tipus. No és un paràmetre jugable: és el llindar
|
||||
// tècnic abans de caure a un fallback aleatori amb advertència.
|
||||
constexpr int MAX_SPAWN_ATTEMPTS = 50;
|
||||
|
||||
// Cuerpo físico común (valores por defecto del constructor)
|
||||
namespace Body {
|
||||
constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0)
|
||||
constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes
|
||||
constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad
|
||||
constexpr float ANGULAR_DAMPING = 0.0F;
|
||||
} // namespace Body
|
||||
|
||||
// Pentagon (esquivador - zigzag evasion)
|
||||
namespace Pentagon {
|
||||
constexpr float SPEED = 35.0F; // px/s (slightly slower)
|
||||
constexpr float MASS = 5.0F; // Masa estándar
|
||||
constexpr float ANGLE_CHANGE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
|
||||
constexpr float ANGLE_CHANGE_MAX = 1.0F; // Max random angle change (rad)
|
||||
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
|
||||
constexpr float ROTATION_DELTA_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
|
||||
constexpr float ROTATION_DELTA_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
|
||||
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
|
||||
} // namespace Pentagon
|
||||
|
||||
// Square (perseguidor - tracks player)
|
||||
namespace Square {
|
||||
constexpr float SPEED = 40.0F; // px/s (medium speed)
|
||||
constexpr float MASS = 8.0F; // Más pesado, "tanque"
|
||||
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
|
||||
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
|
||||
constexpr float ROTATION_DELTA_MIN = 0.3F; // Slow rotation [+50%]
|
||||
constexpr float ROTATION_DELTA_MAX = 1.5F; // [+50%]
|
||||
constexpr const char* SHAPE_FILE = "enemy_square.shp";
|
||||
} // namespace Square
|
||||
|
||||
// Molinillo (agressiu - fast straight lines, proximity spin-up)
|
||||
namespace Pinwheel {
|
||||
constexpr float SPEED = 50.0F; // px/s (fastest)
|
||||
constexpr float MASS = 4.0F; // Más liviano, ágil
|
||||
constexpr float ANGLE_CHANGE_PROB = 0.05F; // 5% per wall hit (rare direction change)
|
||||
constexpr float ANGLE_CHANGE_MAX = 0.3F; // Small angle adjustments
|
||||
constexpr float ROTATION_DELTA_MIN = 3.0F; // Base rotation (rad/s) [+50%]
|
||||
constexpr float ROTATION_DELTA_MAX = 6.0F; // [+50%]
|
||||
constexpr float ROTATION_DELTA_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship
|
||||
constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px)
|
||||
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
|
||||
} // namespace Pinwheel
|
||||
|
||||
// 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
|
||||
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius
|
||||
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
|
||||
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
|
||||
|
||||
// Scoring system (puntuación per type de enemy)
|
||||
namespace Scoring {
|
||||
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
|
||||
constexpr int SQUARE_SCORE = 150; // Square (perseguidor, 40 px/s)
|
||||
constexpr int PINWHEEL_SCORE = 200; // Molinillo (agressiu, 50 px/s)
|
||||
} // namespace Scoring
|
||||
|
||||
} // namespace Defaults::Enemies
|
||||
} // namespace Defaults::Enemies::Spawn
|
||||
|
||||
@@ -8,8 +8,9 @@ namespace Defaults::Entities {
|
||||
constexpr int MAX_ORNIS = 15;
|
||||
constexpr int MAX_BULLETS = 50;
|
||||
|
||||
constexpr float SHIP_RADIUS = 12.0F;
|
||||
constexpr float ENEMY_RADIUS = 20.0F;
|
||||
constexpr float BULLET_RADIUS = 3.0F;
|
||||
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
|
||||
// calcula el seu collision_radius com a
|
||||
// shape.bounding_radius × shape.scale × shape.collision_factor
|
||||
// a partir del seu YAML (data/entities/<name>/<name>.yaml).
|
||||
|
||||
} // namespace Defaults::Entities
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Defaults::Game {
|
||||
// Friendly fire system
|
||||
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
||||
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
|
||||
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS
|
||||
// BULLET_SPEED migrat a data/entities/player/player.yaml (weapon.bullet_speed).
|
||||
|
||||
// Transición LEVEL_START (mensajes aleatorios PRE-level)
|
||||
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
|
||||
|
||||
@@ -14,11 +14,10 @@ namespace Defaults::Palette {
|
||||
// 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ó
|
||||
// visible quan el halo s'expandeix.
|
||||
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
|
||||
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser
|
||||
constexpr SDL_Color PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador"
|
||||
constexpr SDL_Color SQUARE = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank"
|
||||
constexpr SDL_Color PINWHEEL = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu"
|
||||
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
|
||||
// Tots els colors d'entitats han migrat al seu YAML respectiu
|
||||
// (data/entities/<name>/<name>.yaml, secció `colors`):
|
||||
// - SHIP → player.yaml
|
||||
// - PENTAGON / SQUARE / PINWHEEL / WOUNDED → cada enemy.yaml
|
||||
// - BULLET → bullet.yaml
|
||||
|
||||
} // namespace Defaults::Palette
|
||||
|
||||
@@ -3,72 +3,55 @@
|
||||
|
||||
#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).
|
||||
|
||||
constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s)
|
||||
constexpr float ACCELERATION = 400.0F; // px/s²
|
||||
constexpr float MAX_VELOCITY = 180.0F; // px/s
|
||||
constexpr float FRICTION = 20.0F; // px/s²
|
||||
namespace Defaults::Physics::Debris {
|
||||
|
||||
// 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
|
||||
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
|
||||
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
|
||||
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
||||
constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
|
||||
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
|
||||
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
|
||||
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
|
||||
|
||||
// Ship → enemy: impuls explícit aplicat a l'enemic en el moment exacte
|
||||
// que la nau mor per col·lisió amb ell (afegit per damunt del rebot
|
||||
// natural de PhysicsWorld, que ja és present però subtil amb la
|
||||
// damping de la nau).
|
||||
namespace Ship {
|
||||
constexpr float DEATH_IMPACT_MOMENTUM_FACTOR = 0.3F;
|
||||
} // namespace Ship
|
||||
// Política de mort: passat el min_lifetime, el fragment mor quan la
|
||||
// seva velocity cau per sota d'aquest llindar. Així els fragments
|
||||
// ràpids no "popen" en moviment.
|
||||
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
|
||||
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
|
||||
|
||||
// Explosions (debris physics)
|
||||
namespace Debris {
|
||||
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
|
||||
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
|
||||
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
||||
constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
|
||||
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
|
||||
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
|
||||
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
|
||||
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
|
||||
// 0.7 = 70% de l'energia conservada al rebot.
|
||||
constexpr float RESTITUTION_BOUNDS = 0.7F;
|
||||
|
||||
// Política de mort: passat el min_lifetime, el fragment mor quan la
|
||||
// seva velocity cau per sota d'aquest llindar. Així els fragments
|
||||
// ràpids no "popen" en moviment.
|
||||
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
|
||||
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
|
||||
// Herència de velocity angular (trayectorias curvas)
|
||||
constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
||||
constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
|
||||
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
|
||||
|
||||
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
|
||||
// 0.7 = 70% de l'energia conservada al rebot.
|
||||
constexpr float RESTITUTION_BOUNDS = 0.7F;
|
||||
// Velocity heredada de la nau a l'explosió (80% del feel original).
|
||||
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
|
||||
|
||||
// Herència de velocity angular (trayectorias curvas)
|
||||
constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
||||
constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
|
||||
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
|
||||
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
|
||||
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
||||
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
||||
|
||||
// Velocity heredada de la nau a l'explosió (80% del feel original).
|
||||
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
|
||||
// Tuneig específic de l'explosió d'enemic (overrides als defaults
|
||||
// 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_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
|
||||
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
|
||||
|
||||
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
|
||||
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
||||
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
||||
// Angular velocity sin for trajectory inheritance
|
||||
// Excess above this threshold is converted to tangential linear velocity
|
||||
// Prevents "vortex trap" problem with high-rotation enemies
|
||||
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
|
||||
|
||||
// Tuneig específic de l'explosió d'enemic (overrides als defaults
|
||||
// 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_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
|
||||
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
|
||||
|
||||
// Angular velocity sin for trajectory inheritance
|
||||
// Excess above this threshold is converted to tangential linear velocity
|
||||
// Prevents "vortex trap" problem with high-rotation enemies
|
||||
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
|
||||
} // namespace Debris
|
||||
|
||||
} // namespace Defaults::Physics
|
||||
} // namespace Defaults::Physics::Debris
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// ship.hpp - Configuració de la nau (invulnerabilitat, parpelleig)
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Defaults::Ship {
|
||||
|
||||
// Invulnerabilidad post-respawn
|
||||
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
|
||||
|
||||
// Parpadeo visual durante invulnerabilidad
|
||||
constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos)
|
||||
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
|
||||
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
|
||||
|
||||
// Cuerpo físico
|
||||
constexpr float MASS = 10.0F; // Masa de referencia para choques
|
||||
constexpr float RESTITUTION = 0.6F; // Rebote moderado contra paredes
|
||||
constexpr float LINEAR_DAMPING = 1.5F; // Fricción exponencial (s⁻¹)
|
||||
constexpr float ANGULAR_DAMPING = 0.0F; // Rotación 100% por input (no inercial)
|
||||
|
||||
// Empuje visual: escala proporcional a la velocidad (0..200 px/s → 1.0..1.5)
|
||||
// Mantiene la sensación del Pascal original.
|
||||
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
|
||||
constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR)
|
||||
|
||||
// Estat "ferit": entre primera col·lisió amb enemic i recuperació o segona col·lisió mortal.
|
||||
namespace Hurt {
|
||||
constexpr float DURATION = 15.0F; // Segons en estat ferit (provisional)
|
||||
constexpr float BLINK_HZ = 10.0F; // Freqüència parpelleig color normal ↔ ferit
|
||||
} // namespace Hurt
|
||||
|
||||
} // namespace Defaults::Ship
|
||||
@@ -7,7 +7,7 @@ namespace Defaults::Trail {
|
||||
|
||||
constexpr int POOL_SIZE = 200;
|
||||
|
||||
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de Physics::MAX_VELOCITY (180)
|
||||
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de player.yaml::physics.max_velocity (180 px/s)
|
||||
constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal
|
||||
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
|
||||
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// entity_loader.cpp - Implementació del carregador d'entitats YAML
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/entities/entity_loader.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
|
||||
namespace Entities {
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> EntityLoader::cache;
|
||||
|
||||
auto EntityLoader::load(const std::string& name) -> std::shared_ptr<fkyaml::node> {
|
||||
// Cache hit
|
||||
auto it = cache.find(name);
|
||||
if (it != cache.end()) {
|
||||
std::cout << "[EntityLoader] Cache hit: " << name << '\n';
|
||||
return it->second;
|
||||
}
|
||||
|
||||
const std::string PATH = "entities/" + name + "/" + name + ".yaml";
|
||||
|
||||
std::vector<uint8_t> data = Resource::Helper::loadFile(PATH);
|
||||
if (data.empty()) {
|
||||
std::cerr << "[EntityLoader] Error: no s'ha pogut load " << PATH << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
try {
|
||||
std::string yaml_content(data.begin(), data.end());
|
||||
std::stringstream stream(yaml_content);
|
||||
auto node = std::make_shared<fkyaml::node>(fkyaml::node::deserialize(stream));
|
||||
|
||||
std::cout << "[EntityLoader] Carregat: " << PATH << '\n';
|
||||
cache[name] = node;
|
||||
return node;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[EntityLoader] Excepció parsejant " << PATH << ": " << e.what() << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void EntityLoader::clearCache() {
|
||||
std::cout << "[EntityLoader] Netejant caché (" << cache.size() << " entitats)" << '\n';
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
auto EntityLoader::getCacheSize() -> size_t { return cache.size(); }
|
||||
|
||||
} // namespace Entities
|
||||
@@ -0,0 +1,38 @@
|
||||
// entity_loader.hpp - Carregador genèric de descriptors d'entitats en YAML
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Cada entitat viu a `data/entities/<name>/<name>.yaml` (mateix patró que el
|
||||
// projecte germà aee_arcade). Aquest loader resol el path, llegeix del
|
||||
// resource pack via Resource::Helper, parseja amb fkyaml i cacheja el node
|
||||
// per evitar relectures. Retorna nullptr en cas d'error (el caller decideix
|
||||
// si abortar).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace Entities {
|
||||
|
||||
class EntityLoader {
|
||||
public:
|
||||
EntityLoader() = delete; // tot estàtic
|
||||
|
||||
// Carrega el descriptor d'una entitat per nom (ex. "player" →
|
||||
// "entities/player/player.yaml"). Retorna nullptr si no es pot
|
||||
// carregar o parsejar. Cachejat per nom.
|
||||
static auto load(const std::string& name) -> std::shared_ptr<fkyaml::node>;
|
||||
|
||||
// Buidar caché (útil per debug/recàrrega).
|
||||
static void clearCache();
|
||||
|
||||
[[nodiscard]] static auto getCacheSize() -> size_t;
|
||||
|
||||
private:
|
||||
static std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> cache;
|
||||
};
|
||||
|
||||
} // namespace Entities
|
||||
@@ -53,7 +53,8 @@ namespace Graphics {
|
||||
|
||||
// Cache and return
|
||||
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName()
|
||||
<< ", " << shape->getNumPrimitives() << " primitives)" << '\n';
|
||||
<< ", " << shape->getNumPrimitives() << " primitives, bounding_radius="
|
||||
<< shape->getBoundingRadius() << ")" << '\n';
|
||||
|
||||
cache[filename] = shape;
|
||||
return shape;
|
||||
|
||||
@@ -10,300 +10,287 @@
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Calcular checksum CRC32 simplificat
|
||||
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
|
||||
uint32_t checksum = 0x12345678;
|
||||
for (unsigned char byte : data) {
|
||||
checksum = ((checksum << 5) + checksum) + byte;
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
// Encriptació XOR (simètrica)
|
||||
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
if (key.empty()) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
data[i] ^= key[i % key.length()];
|
||||
}
|
||||
}
|
||||
|
||||
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
// XOR es simètric
|
||||
encryptData(data, key);
|
||||
}
|
||||
|
||||
// Llegir file complet a memòria
|
||||
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
|
||||
return {};
|
||||
// Calcular checksum CRC32 simplificat
|
||||
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
|
||||
uint32_t checksum = 0x12345678;
|
||||
for (unsigned char byte : data) {
|
||||
checksum = ((checksum << 5) + checksum) + byte;
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
std::streamsize file_size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<uint8_t> data(file_size);
|
||||
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
|
||||
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n';
|
||||
return {};
|
||||
// Encriptació XOR (simètrica)
|
||||
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
if (key.empty()) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
data[i] ^= key[i % key.length()];
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Añadir un file individual al paquet
|
||||
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
|
||||
auto file_data = readFile(filepath);
|
||||
if (file_data.empty()) {
|
||||
return false;
|
||||
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
// XOR es simètric
|
||||
encryptData(data, key);
|
||||
}
|
||||
|
||||
ResourceEntry entry{
|
||||
.filename = pack_name,
|
||||
.offset = data_.size(),
|
||||
.size = file_data.size(),
|
||||
.checksum = calculateChecksum(file_data)};
|
||||
|
||||
// Añadir dades al bloc de dades
|
||||
data_.insert(data_.end(), file_data.begin(), file_data.end());
|
||||
|
||||
resources_[pack_name] = entry;
|
||||
|
||||
std::cout << "[ResourcePack] Añadido: " << pack_name << " (" << file_data.size()
|
||||
<< " bytes)\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Añadir todos los archivos de un directori recursivament
|
||||
auto Pack::addDirectory(const std::string& dir_path,
|
||||
const std::string& base_path) -> bool {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
|
||||
std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string current_base = base_path.empty() ? "" : base_path + "/";
|
||||
|
||||
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
// Llegir file complet a memòria
|
||||
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string full_path = entry.path().string();
|
||||
std::string relative_path = entry.path().lexically_relative(dir_path).string();
|
||||
std::streamsize file_size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
// Convertir barres invertides a normals (Windows)
|
||||
std::ranges::replace(relative_path, '\\', '/');
|
||||
|
||||
// Saltar archivos de desenvolupament
|
||||
if (relative_path.find(".world") != std::string::npos ||
|
||||
relative_path.find(".tsx") != std::string::npos ||
|
||||
relative_path.find(".DS_Store") != std::string::npos ||
|
||||
relative_path.find(".git") != std::string::npos) {
|
||||
std::cout << "[ResourcePack] Saltant: " << relative_path << '\n';
|
||||
continue;
|
||||
std::vector<uint8_t> data(file_size);
|
||||
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
|
||||
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string pack_name = current_base + relative_path;
|
||||
addFile(full_path, pack_name);
|
||||
return data;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
// Añadir un file individual al paquet
|
||||
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
|
||||
auto file_data = readFile(filepath);
|
||||
if (file_data.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Guardar paquet a disc
|
||||
auto Pack::savePack(const std::string& pack_file) -> bool {
|
||||
std::ofstream file(pack_file, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n';
|
||||
return false;
|
||||
ResourceEntry entry{
|
||||
.filename = pack_name,
|
||||
.offset = data_.size(),
|
||||
.size = file_data.size(),
|
||||
.checksum = calculateChecksum(file_data)};
|
||||
|
||||
// Añadir dades al bloc de dades
|
||||
data_.insert(data_.end(), file_data.begin(), file_data.end());
|
||||
|
||||
resources_[pack_name] = entry;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Escriure capçalera
|
||||
file.write(MAGIC_HEADER, 4);
|
||||
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
|
||||
// Añadir todos los archivos de un directori recursivament
|
||||
auto Pack::addDirectory(const std::string& dir_path,
|
||||
const std::string& base_path) -> bool {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Escriure nombre de recursos
|
||||
auto resource_count = static_cast<uint32_t>(resources_.size());
|
||||
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
|
||||
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
|
||||
std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Escriure metadades de recursos
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
// Nom del file
|
||||
auto name_len = static_cast<uint32_t>(entry.filename.length());
|
||||
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
|
||||
file.write(entry.filename.c_str(), name_len);
|
||||
std::string current_base = base_path.empty() ? "" : base_path + "/";
|
||||
|
||||
// Offset, mida, checksum
|
||||
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
|
||||
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
|
||||
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
|
||||
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string full_path = entry.path().string();
|
||||
std::string relative_path = entry.path().lexically_relative(dir_path).string();
|
||||
|
||||
// Convertir barres invertides a normals (Windows)
|
||||
std::ranges::replace(relative_path, '\\', '/');
|
||||
|
||||
// Saltar archivos de desenvolupament
|
||||
if (relative_path.find(".world") != std::string::npos ||
|
||||
relative_path.find(".tsx") != std::string::npos ||
|
||||
relative_path.find(".DS_Store") != std::string::npos ||
|
||||
relative_path.find(".git") != std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string pack_name = current_base + relative_path;
|
||||
addFile(full_path, pack_name);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Encriptar dades
|
||||
std::vector<uint8_t> encrypted_data = data_;
|
||||
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
|
||||
// Guardar paquet a disc
|
||||
auto Pack::savePack(const std::string& pack_file) -> bool {
|
||||
std::ofstream file(pack_file, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Escriure mida de dades y dades encriptades
|
||||
auto data_size = static_cast<uint64_t>(encrypted_data.size());
|
||||
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
|
||||
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
|
||||
// Escriure capçalera
|
||||
file.write(MAGIC_HEADER, 4);
|
||||
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
|
||||
|
||||
std::cout << "[ResourcePack] Guardat: " << pack_file << " (" << resources_.size()
|
||||
<< " recursos, " << data_size << " bytes)\n";
|
||||
// Escriure nombre de recursos
|
||||
auto resource_count = static_cast<uint32_t>(resources_.size());
|
||||
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
|
||||
|
||||
return true;
|
||||
}
|
||||
// Escriure metadades de recursos
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
// Nom del file
|
||||
auto name_len = static_cast<uint32_t>(entry.filename.length());
|
||||
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
|
||||
file.write(entry.filename.c_str(), name_len);
|
||||
|
||||
// Carregar paquet desde disc
|
||||
auto Pack::loadPack(const std::string& pack_file) -> bool {
|
||||
std::ifstream file(pack_file, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
|
||||
return false;
|
||||
// Offset, mida, checksum
|
||||
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
|
||||
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
|
||||
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
|
||||
}
|
||||
|
||||
// Encriptar dades
|
||||
std::vector<uint8_t> encrypted_data = data_;
|
||||
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
|
||||
|
||||
// Escriure mida de dades y dades encriptades
|
||||
auto data_size = static_cast<uint64_t>(encrypted_data.size());
|
||||
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
|
||||
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Llegir capçalera
|
||||
char magic[4];
|
||||
file.read(magic, 4);
|
||||
if (std::string(magic, 4) != MAGIC_HEADER) {
|
||||
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER
|
||||
<< ")\n";
|
||||
return false;
|
||||
// Carregar paquet desde disc
|
||||
auto Pack::loadPack(const std::string& pack_file) -> bool {
|
||||
std::ifstream file(pack_file, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Llegir capçalera
|
||||
char magic[4];
|
||||
file.read(magic, 4);
|
||||
if (std::string(magic, 4) != MAGIC_HEADER) {
|
||||
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER
|
||||
<< ")\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t version;
|
||||
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||
if (version != VERSION) {
|
||||
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION
|
||||
<< ", trobat " << version << ")\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Llegir nombre de recursos
|
||||
uint32_t resource_count;
|
||||
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
|
||||
|
||||
// Llegir metadades de recursos
|
||||
resources_.clear();
|
||||
for (uint32_t i = 0; i < resource_count; ++i) {
|
||||
// Nom del file
|
||||
uint32_t name_len;
|
||||
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
|
||||
|
||||
std::string filename(name_len, '\0');
|
||||
file.read(filename.data(), name_len);
|
||||
|
||||
// Offset, mida, checksum
|
||||
ResourceEntry entry;
|
||||
entry.filename = filename;
|
||||
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
|
||||
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
|
||||
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
|
||||
|
||||
resources_[filename] = entry;
|
||||
}
|
||||
|
||||
// Llegir dades encriptades
|
||||
uint64_t data_size;
|
||||
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
|
||||
|
||||
data_.resize(data_size);
|
||||
file.read(reinterpret_cast<char*>(data_.data()), data_size);
|
||||
|
||||
// Desencriptar
|
||||
decryptData(data_, DEFAULT_ENCRYPT_KEY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t version;
|
||||
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||
if (version != VERSION) {
|
||||
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION
|
||||
<< ", trobat " << version << ")\n";
|
||||
return false;
|
||||
}
|
||||
// Obtenir un recurs del paquet
|
||||
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||
auto it = resources_.find(filename);
|
||||
if (it == resources_.end()) {
|
||||
std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
// Llegir nombre de recursos
|
||||
uint32_t resource_count;
|
||||
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
|
||||
const auto& entry = it->second;
|
||||
|
||||
// Llegir metadades de recursos
|
||||
resources_.clear();
|
||||
for (uint32_t i = 0; i < resource_count; ++i) {
|
||||
// Nom del file
|
||||
uint32_t name_len;
|
||||
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
|
||||
|
||||
std::string filename(name_len, '\0');
|
||||
file.read(filename.data(), name_len);
|
||||
|
||||
// Offset, mida, checksum
|
||||
ResourceEntry entry;
|
||||
entry.filename = filename;
|
||||
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
|
||||
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
|
||||
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
|
||||
|
||||
resources_[filename] = entry;
|
||||
}
|
||||
|
||||
// Llegir dades encriptades
|
||||
uint64_t data_size;
|
||||
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
|
||||
|
||||
data_.resize(data_size);
|
||||
file.read(reinterpret_cast<char*>(data_.data()), data_size);
|
||||
|
||||
// Desencriptar
|
||||
decryptData(data_, DEFAULT_ENCRYPT_KEY);
|
||||
|
||||
std::cout << "[ResourcePack] Carregat: " << pack_file << " (" << resources_.size()
|
||||
<< " recursos)\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Obtenir un recurs del paquet
|
||||
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||
auto it = resources_.find(filename);
|
||||
if (it == resources_.end()) {
|
||||
std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto& entry = it->second;
|
||||
|
||||
// Extreure dades
|
||||
if (entry.offset + entry.size > data_.size()) {
|
||||
std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
|
||||
data_.begin() + entry.offset + entry.size);
|
||||
|
||||
// Verificar checksum
|
||||
uint32_t computed_checksum = calculateChecksum(resource_data);
|
||||
if (computed_checksum != entry.checksum) {
|
||||
std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
|
||||
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum
|
||||
<< ")\n";
|
||||
// No falla, pero adverteix
|
||||
}
|
||||
|
||||
return resource_data;
|
||||
}
|
||||
|
||||
// Comprovar si existeix un recurs
|
||||
auto Pack::hasResource(const std::string& filename) const -> bool {
|
||||
return resources_.contains(filename);
|
||||
}
|
||||
|
||||
// Obtenir list de todos los recursos
|
||||
auto Pack::getResourceList() const -> std::vector<std::string> {
|
||||
std::vector<std::string> list;
|
||||
list.reserve(resources_.size());
|
||||
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
list.push_back(name);
|
||||
}
|
||||
|
||||
std::ranges::sort(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
// Validar integritat del paquet
|
||||
auto Pack::validatePack() const -> bool {
|
||||
bool valid = true;
|
||||
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
// Verificar offset i mida
|
||||
// Extreure dades
|
||||
if (entry.offset + entry.size > data_.size()) {
|
||||
std::cerr << "[ResourcePack] Error de validació: " << name
|
||||
<< " té offset/mida invàlid\n";
|
||||
valid = false;
|
||||
continue;
|
||||
std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
// Extreure i verificar checksum
|
||||
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
|
||||
data_.begin() + entry.offset + entry.size);
|
||||
|
||||
// Verificar checksum
|
||||
uint32_t computed_checksum = calculateChecksum(resource_data);
|
||||
if (computed_checksum != entry.checksum) {
|
||||
std::cerr << "[ResourcePack] Error de validació: " << name
|
||||
<< " té checksum invàlid\n";
|
||||
valid = false;
|
||||
std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
|
||||
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum
|
||||
<< ")\n";
|
||||
// No falla, pero adverteix
|
||||
}
|
||||
|
||||
return resource_data;
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
std::cout << "[ResourcePack] Validació OK (" << resources_.size() << " recursos)\n";
|
||||
// Comprovar si existeix un recurs
|
||||
auto Pack::hasResource(const std::string& filename) const -> bool {
|
||||
return resources_.contains(filename);
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
// Obtenir list de todos los recursos
|
||||
auto Pack::getResourceList() const -> std::vector<std::string> {
|
||||
std::vector<std::string> list;
|
||||
list.reserve(resources_.size());
|
||||
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
list.push_back(name);
|
||||
}
|
||||
|
||||
std::ranges::sort(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
// Validar integritat del paquet
|
||||
auto Pack::validatePack() const -> bool {
|
||||
bool valid = true;
|
||||
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
// Verificar offset i mida
|
||||
if (entry.offset + entry.size > data_.size()) {
|
||||
std::cerr << "[ResourcePack] Error de validació: " << name
|
||||
<< " té offset/mida invàlid\n";
|
||||
valid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extreure i verificar checksum
|
||||
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
|
||||
data_.begin() + entry.offset + entry.size);
|
||||
|
||||
uint32_t computed_checksum = calculateChecksum(resource_data);
|
||||
if (computed_checksum != entry.checksum) {
|
||||
std::cerr << "[ResourcePack] Error de validació: " << name
|
||||
<< " té checksum invàlid\n";
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
} // namespace Resource
|
||||
|
||||
@@ -14,38 +14,39 @@
|
||||
#include "core/rendering/shape_renderer.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/constants.hpp"
|
||||
#include "game/entities/bullet_config.hpp"
|
||||
#include "game/entities/bullet_registry.hpp"
|
||||
|
||||
Bullet::Bullet(Rendering::Renderer* renderer)
|
||||
: Entity(renderer) {
|
||||
// Brightness específico para balas
|
||||
: Entity(renderer),
|
||||
config_(&BulletRegistry::get()) {
|
||||
brightness_ = Defaults::Brightness::BALA;
|
||||
|
||||
// Configuración del cuerpo físico.
|
||||
// Las balas son cinemáticas: no colisionan con otros bodies ni paredes.
|
||||
// El gameplay (GameScene) gestiona los hits con check_collision y la
|
||||
// salida del PLAYAREA. Por eso radius=0 en el world (no participa en
|
||||
// resolveBodyCollisions ni resolveBoundsCollisions).
|
||||
body_.setMass(0.5F); // Ligera (no afecta a nadie, pero por consistencia)
|
||||
body_.radius = 0.0F; // Sin colisión física (cinemática pura)
|
||||
body_.restitution = 0.0F; // Irrelevante (no rebota)
|
||||
body_.linear_damping = 0.0F; // Sin fricción (movimiento rectilíneo uniforme)
|
||||
body_.angular_damping = 0.0F;
|
||||
// Cinemàtiques pures: no col·lisionen al PhysicsWorld (body_.radius = 0).
|
||||
// El gameplay (GameScene) gestiona els hits via checkCollisionSwept i la
|
||||
// sortida del PLAYAREA.
|
||||
body_.setMass(config_->physics.mass);
|
||||
body_.radius = 0.0F;
|
||||
body_.restitution = config_->physics.restitution;
|
||||
body_.linear_damping = config_->physics.linear_damping;
|
||||
body_.angular_damping = config_->physics.angular_damping;
|
||||
|
||||
// Cargar shape compartida desde archivo
|
||||
shape_ = Graphics::ShapeLoader::load("bullet.shp");
|
||||
shape_ = Graphics::ShapeLoader::load(config_->shape.path);
|
||||
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() {
|
||||
// Inicialment inactiva
|
||||
is_active_ = false;
|
||||
center_ = {.x = 0.0F, .y = 0.0F};
|
||||
prev_position_ = {.x = 0.0F, .y = 0.0F};
|
||||
angle_ = 0.0F;
|
||||
|
||||
// Reset del cuerpo físico
|
||||
body_.position = Vec2{};
|
||||
body_.velocity = Vec2{};
|
||||
body_.angle = 0.0F;
|
||||
@@ -53,29 +54,24 @@ void Bullet::init() {
|
||||
body_.clearAccumulators();
|
||||
}
|
||||
|
||||
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id) {
|
||||
// Activar bullet
|
||||
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed) {
|
||||
is_active_ = true;
|
||||
|
||||
// Almacenar propietario (0=P1, 1=P2)
|
||||
owner_id_ = owner_id;
|
||||
|
||||
// Posición y orientación iniciales = ship
|
||||
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;
|
||||
|
||||
// Sincronizar el body físico: posición + velocidad cartesiana
|
||||
// angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL)
|
||||
// Sincronizar el body físic: posició + velocitat cartesiana.
|
||||
// angle - PI/2 perquè angle=0 apunta cap amunt (eje Y negatiu SDL).
|
||||
body_.position = position;
|
||||
body_.angle = angle;
|
||||
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
|
||||
const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F));
|
||||
body_.velocity = Vec2{.x = DIR_X * Defaults::Game::BULLET_SPEED, .y = DIR_Y * Defaults::Game::BULLET_SPEED};
|
||||
body_.velocity = Vec2{.x = DIR_X * bullet_speed, .y = DIR_Y * bullet_speed};
|
||||
body_.angular_velocity = 0.0F;
|
||||
body_.clearAccumulators();
|
||||
|
||||
// Reproducir sonido de disparo láser
|
||||
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*/) {
|
||||
// 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_;
|
||||
center_ = body_.position;
|
||||
// angle_ no cambia (las balas no rotan visualmente).
|
||||
}
|
||||
|
||||
void Bullet::desactivar() {
|
||||
is_active_ = false;
|
||||
// Detener el cuerpo físico para que no acumule deriva mientras inactiva.
|
||||
body_.velocity = Vec2{};
|
||||
body_.angular_velocity = 0.0F;
|
||||
}
|
||||
|
||||
void Bullet::draw() const {
|
||||
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_, Defaults::Palette::BULLET);
|
||||
Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, config_->colors.normal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/entities/entity.hpp"
|
||||
#include "core/types.hpp"
|
||||
|
||||
// Forward declaration — la definició completa s'inclou només al .cpp.
|
||||
struct BulletConfig;
|
||||
|
||||
class Bullet : public Entities::Entity {
|
||||
public:
|
||||
Bullet()
|
||||
@@ -17,7 +19,7 @@ class Bullet : public Entities::Entity {
|
||||
explicit Bullet(Rendering::Renderer* renderer);
|
||||
|
||||
void init() override;
|
||||
void fire(const Vec2& position, float angle, uint8_t owner_id);
|
||||
void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed);
|
||||
void update(float delta_time) override;
|
||||
void postUpdate(float delta_time) override;
|
||||
void draw() const override;
|
||||
@@ -25,25 +27,25 @@ class Bullet : public Entities::Entity {
|
||||
// Override: Interfaz de Entity
|
||||
[[nodiscard]] auto isActive() const -> bool override { return is_active_; }
|
||||
|
||||
// Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check)
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override {
|
||||
return Defaults::Entities::BULLET_RADIUS;
|
||||
}
|
||||
// Override: Interfaz de colisión (radi derivat al ctor des del shape).
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
|
||||
[[nodiscard]] auto isCollidable() const -> bool override {
|
||||
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)
|
||||
[[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_; }
|
||||
void desactivar();
|
||||
|
||||
private:
|
||||
// Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
|
||||
// Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer
|
||||
// dejen el objeto en estado coherente (proyectil inactivo, sin owner).
|
||||
const BulletConfig* config_{nullptr}; // apunta al BulletRegistry; vàlid post-ctor
|
||||
float collision_radius_{0.0F}; // derivat: shape.bounding_radius × scale × collision_factor
|
||||
|
||||
bool is_active_{false};
|
||||
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)
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
+88
-185
@@ -14,6 +14,8 @@
|
||||
#include "core/rendering/shape_renderer.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/constants.hpp"
|
||||
#include "game/entities/enemy_config.hpp"
|
||||
#include "game/entities/enemy_registry.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -27,99 +29,72 @@ namespace {
|
||||
}
|
||||
|
||||
// Recupera el "ángulo equivalente" de un body en movimiento (para zigzag).
|
||||
// Si está parado, devuelve 0.
|
||||
auto velocityToAngle(const Vec2& velocity) -> float {
|
||||
if (velocity.lengthSquared() < 0.0001F) {
|
||||
return 0.0F;
|
||||
}
|
||||
// El movimiento (vx, vy) corresponde a angle - PI/2; invertimos.
|
||||
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
|
||||
|
||||
Enemy::Enemy(Rendering::Renderer* renderer)
|
||||
: Entity(renderer),
|
||||
|
||||
tracking_strength_(Defaults::Enemies::Square::TRACKING_STRENGTH) {
|
||||
: Entity(renderer) {
|
||||
brightness_ = Defaults::Brightness::ENEMIC;
|
||||
|
||||
// Configuración del cuerpo físico — defaults para enemy genérico.
|
||||
// init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo).
|
||||
body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS);
|
||||
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
|
||||
body_.restitution = Defaults::Enemies::Body::RESTITUTION;
|
||||
body_.linear_damping = Defaults::Enemies::Body::LINEAR_DAMPING;
|
||||
body_.angular_damping = Defaults::Enemies::Body::ANGULAR_DAMPING;
|
||||
// Body queda amb defaults inocus (radius=0 = no col·lisiona) fins
|
||||
// que init() apliqui la configuració del tipus carregada via Registry.
|
||||
body_.radius = 0.0F;
|
||||
}
|
||||
|
||||
void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
type_ = type;
|
||||
config_ = &EnemyRegistry::get(type);
|
||||
const EnemyConfig& cfg = *config_;
|
||||
|
||||
const char* shape_file = nullptr;
|
||||
float base_speed = 0.0F;
|
||||
float rotation_delta_min = 0.0F;
|
||||
float rotation_delta_max = 0.0F;
|
||||
float type_mass = Defaults::Enemies::Body::DEFAULT_MASS;
|
||||
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Pentagon::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
|
||||
type_mass = Defaults::Enemies::Pentagon::MASS;
|
||||
break;
|
||||
|
||||
case EnemyType::SQUARE:
|
||||
shape_file = Defaults::Enemies::Square::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Square::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Square::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Square::ROTATION_DELTA_MAX;
|
||||
type_mass = Defaults::Enemies::Square::MASS;
|
||||
tracking_timer_ = 0.0F;
|
||||
break;
|
||||
|
||||
case EnemyType::PINWHEEL:
|
||||
shape_file = Defaults::Enemies::Pinwheel::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Pinwheel::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MAX;
|
||||
type_mass = Defaults::Enemies::Pinwheel::MASS;
|
||||
break;
|
||||
|
||||
default:
|
||||
std::cerr << "[Enemy] Error: tipo desconocido ("
|
||||
<< static_cast<int>(type_) << "), usando PENTAGON\n";
|
||||
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Pentagon::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
|
||||
break;
|
||||
if (type_ == EnemyType::SQUARE) {
|
||||
tracking_timer_ = 0.0F;
|
||||
tracking_strength_ = cfg.behavior.tracking_strength;
|
||||
}
|
||||
|
||||
body_.setMass(type_mass);
|
||||
body_.radius = Defaults::Entities::ENEMY_RADIUS;
|
||||
|
||||
// Cargar shape
|
||||
shape_ = Graphics::ShapeLoader::load(shape_file);
|
||||
shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
|
||||
if (!shape_ || !shape_->isValid()) {
|
||||
std::cerr << "[Enemy] Error: no se ha podido cargar " << shape_file << '\n';
|
||||
std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n';
|
||||
}
|
||||
|
||||
// Posición aleatoria con comprobación de seguridad
|
||||
// Radi de col·lisió derivat del cercle circumscrit de la shape.
|
||||
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
|
||||
collision_radius_ = BOUNDING * cfg.shape.scale * cfg.shape.collision_factor;
|
||||
|
||||
body_.setMass(cfg.physics.mass);
|
||||
body_.radius = collision_radius_;
|
||||
body_.restitution = cfg.physics.restitution;
|
||||
body_.linear_damping = cfg.physics.linear_damping;
|
||||
body_.angular_damping = cfg.physics.angular_damping;
|
||||
|
||||
// Posició aleatòria amb comprovació de safety_distance.
|
||||
float min_x;
|
||||
float max_x;
|
||||
float min_y;
|
||||
float max_y;
|
||||
Constants::getSafePlayAreaBounds(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y);
|
||||
Constants::getSafePlayAreaBounds(collision_radius_, min_x, max_x, min_y, max_y);
|
||||
|
||||
if (ship_pos != nullptr) {
|
||||
bool found_safe_position = false;
|
||||
for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) {
|
||||
float candidate_x;
|
||||
float candidate_y;
|
||||
if (attemptSafeSpawn(*ship_pos, candidate_x, candidate_y)) {
|
||||
if (attemptSafeSpawn(*ship_pos, collision_radius_, cfg.spawn.safety_distance, candidate_x, candidate_y)) {
|
||||
center_.x = candidate_x;
|
||||
center_.y = candidate_y;
|
||||
found_safe_position = true;
|
||||
@@ -141,32 +116,26 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
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 = (std::rand() % 360) * Constants::PI / 180.0F;
|
||||
setVelocityFromAngle(ANGLE_INICIAL, base_speed);
|
||||
const float ANGLE_INICIAL = static_cast<float>(std::rand() % 360) * Constants::PI / 180.0F;
|
||||
setVelocityFromAngle(ANGLE_INICIAL, cfg.physics.speed);
|
||||
|
||||
// Sincronizar body_ con posición inicial
|
||||
body_.position = center_;
|
||||
body_.angle = 0.0F;
|
||||
body_.angular_velocity = 0.0F;
|
||||
body_.clearAccumulators();
|
||||
|
||||
// Rotación visual aleatoria (independiente del body)
|
||||
const float ROTATION_DELTA_RANGE = rotation_delta_max - rotation_delta_min;
|
||||
rotation_delta_ = rotation_delta_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_DELTA_RANGE);
|
||||
// Rotació visual aleatòria dins del rang del tipus
|
||||
rotation_delta_ = randRange(cfg.physics.rotation_delta_min, cfg.physics.rotation_delta_max);
|
||||
rotation_ = 0.0F;
|
||||
|
||||
// Estado de animación
|
||||
animation_ = EnemyAnimation();
|
||||
animation_.rotation_delta_base = rotation_delta_;
|
||||
animation_.rotation_delta_target = rotation_delta_;
|
||||
animation_.rotation_delta_t = 1.0F;
|
||||
|
||||
// Invulnerabilidad post-spawn
|
||||
invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
|
||||
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
|
||||
invulnerability_timer_ = cfg.spawn.invulnerability_duration;
|
||||
brightness_ = cfg.spawn.invulnerability_brightness_start;
|
||||
|
||||
// Timer para próximo cambio de dirección (Pentagon)
|
||||
direction_change_timer_ = 0.0F;
|
||||
|
||||
is_active_ = true;
|
||||
@@ -177,8 +146,6 @@ void Enemy::update(float delta_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Decremento de timer "herido"; al cruzar 0 marca expiración para que el
|
||||
// system layer dispare la explosión diferida.
|
||||
wound_expired_this_frame_ = false;
|
||||
if (wounded_timer_ > 0.0F) {
|
||||
wounded_timer_ -= delta_time;
|
||||
@@ -188,25 +155,24 @@ void Enemy::update(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
// Decremento de invulnerabilidad + LERP de brightness
|
||||
if (invulnerability_timer_ > 0.0F) {
|
||||
invulnerability_timer_ -= delta_time;
|
||||
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 SMOOTH_T = T * T * (3.0F - (2.0F * T));
|
||||
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
|
||||
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_END;
|
||||
const float START = config_->spawn.invulnerability_brightness_start;
|
||||
const float END = config_->spawn.invulnerability_brightness_end;
|
||||
brightness_ = START + ((END - START) * SMOOTH_T);
|
||||
}
|
||||
|
||||
// Comportamiento por tipo (ajusta body_.velocity, NO mueve posición).
|
||||
// Skip cuando está herido: el enemy és un "cos mort" inert, només
|
||||
// respon a la inèrcia del impulse rebut i a les col·lisions físiques.
|
||||
if (!isWounded()) {
|
||||
switch (type_) {
|
||||
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);
|
||||
break;
|
||||
case EnemyType::SQUARE:
|
||||
@@ -218,15 +184,12 @@ void Enemy::update(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
// Animaciones (palpitación + rotación acelerada)
|
||||
updateAnimation(delta_time);
|
||||
|
||||
// Rotación visual (decoración, no afecta movimiento)
|
||||
rotation_ += rotation_delta_ * delta_time;
|
||||
}
|
||||
|
||||
void Enemy::postUpdate(float /*delta_time*/) {
|
||||
// Sincronizar mirror tras la integración del world.
|
||||
if (is_active_) {
|
||||
center_ = body_.position;
|
||||
}
|
||||
@@ -236,27 +199,14 @@ void Enemy::draw() const {
|
||||
if (!is_active_ || !shape_) {
|
||||
return;
|
||||
}
|
||||
const float SCALE = computeCurrentScale();
|
||||
SDL_Color color{};
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
color = Defaults::Palette::PENTAGON;
|
||||
break;
|
||||
case EnemyType::SQUARE:
|
||||
color = Defaults::Palette::SQUARE;
|
||||
break;
|
||||
case EnemyType::PINWHEEL:
|
||||
color = Defaults::Palette::PINWHEEL;
|
||||
break;
|
||||
}
|
||||
const float SCALE = config_->shape.scale * computeCurrentScale();
|
||||
SDL_Color color = config_->colors.normal;
|
||||
|
||||
// Parpadeo dorado mientras está herido: alterna color de tipo ↔ dorado
|
||||
// a Wounded::BLINK_HZ usando el timer (fmod sobre el periodo).
|
||||
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);
|
||||
if (T < (CYCLE / 2.0F)) {
|
||||
color = Defaults::Palette::WOUNDED;
|
||||
color = config_->colors.wounded;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,17 +217,15 @@ void Enemy::destroy() {
|
||||
is_active_ = false;
|
||||
body_.velocity = Vec2{};
|
||||
body_.angular_velocity = 0.0F;
|
||||
body_.radius = 0.0F; // No colisiona mientras está inactivo
|
||||
body_.radius = 0.0F;
|
||||
wounded_timer_ = 0.0F;
|
||||
wound_expired_this_frame_ = false;
|
||||
last_hit_by_ = 0xFF;
|
||||
}
|
||||
|
||||
void Enemy::hurt(uint8_t shooter_id) {
|
||||
wounded_timer_ = Defaults::Enemies::Wounded::DURATION;
|
||||
wounded_timer_ = config_->wounded.duration;
|
||||
last_hit_by_ = shooter_id;
|
||||
// El so HIT ara el reprodueix la bala quan es trenca en debris
|
||||
// (Systems::Collision::breakBullet), no l'enemic en entrar a HURT.
|
||||
}
|
||||
|
||||
void Enemy::applyImpulse(const Vec2& impulse) {
|
||||
@@ -285,13 +233,11 @@ void Enemy::applyImpulse(const Vec2& impulse) {
|
||||
}
|
||||
|
||||
void Enemy::setVelocity(float speed) {
|
||||
// Mantener la dirección actual del body, cambiar solo la magnitud.
|
||||
const float CURRENT_SPEED = body_.velocity.length();
|
||||
if (CURRENT_SPEED > 0.0F) {
|
||||
body_.velocity = body_.velocity * (speed / CURRENT_SPEED);
|
||||
} else {
|
||||
// Sin dirección actual: usar ángulo aleatorio
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -300,19 +246,14 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
|
||||
body_.velocity = angleToDirection(angle_movement) * speed;
|
||||
}
|
||||
|
||||
// PENTAGON: zigzag esquivador. Cambios de dirección periódicos (probabilísticos)
|
||||
// en lugar de detectar paredes; el rebote contra muros lo hace PhysicsWorld
|
||||
// con restitution=1.0.
|
||||
// PENTAGON: zigzag esquivador. Canvis de direcció periòdics (probabilístics)
|
||||
// en lloc de detectar parets; el rebot contra murs el fa PhysicsWorld.
|
||||
void Enemy::behaviorPentagon(float delta_time) {
|
||||
direction_change_timer_ += delta_time;
|
||||
|
||||
// Probabilidad de zigzag por segundo (calibrada para sensación equivalente
|
||||
// a la versión vieja que disparaba en cada toque de pared).
|
||||
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||
if (RAND_VAL < Defaults::Enemies::Pentagon::ZIGZAG_PROB_PER_SECOND * delta_time) {
|
||||
if (randFloat01() < config_->behavior.zigzag_prob_per_second * delta_time) {
|
||||
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
|
||||
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) *
|
||||
Defaults::Enemies::Pentagon::ANGLE_CHANGE_MAX;
|
||||
const float DELTA = randFloat01() * config_->behavior.angle_change_max;
|
||||
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
|
||||
const float SPEED = body_.velocity.length();
|
||||
setVelocityFromAngle(NEW_ANGLE, SPEED);
|
||||
@@ -320,12 +261,11 @@ void Enemy::behaviorPentagon(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
// SQUARE: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección
|
||||
// hacia el ship mezclando con tracking_strength_.
|
||||
// SQUARE: tracking discret cap a la nau cada N segons.
|
||||
void Enemy::behaviorSquare(float delta_time) {
|
||||
tracking_timer_ += delta_time;
|
||||
|
||||
if (tracking_timer_ >= Defaults::Enemies::Square::TRACKING_INTERVAL && ship_position_ != nullptr) {
|
||||
if (tracking_timer_ >= config_->behavior.tracking_interval && ship_position_ != nullptr) {
|
||||
tracking_timer_ = 0.0F;
|
||||
|
||||
const Vec2 TO_SHIP = *ship_position_ - center_;
|
||||
@@ -335,11 +275,9 @@ void Enemy::behaviorSquare(float delta_time) {
|
||||
const float SPEED = body_.velocity.length();
|
||||
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED;
|
||||
|
||||
// Mezcla LERP: velocidad actual con la deseada según tracking_strength_.
|
||||
body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
|
||||
(DESIRED_VEL * tracking_strength_);
|
||||
|
||||
// Renormalizar a la velocidad escalar original
|
||||
const float NEW_SPEED = body_.velocity.length();
|
||||
if (NEW_SPEED > 0.0F) {
|
||||
body_.velocity = body_.velocity * (SPEED / NEW_SPEED);
|
||||
@@ -348,21 +286,17 @@ void Enemy::behaviorSquare(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
// PINWHEEL: movimiento recto + boost de rotación visual cerca del ship.
|
||||
// Sin tracking — solo cambios de dirección raros (igual que Pentagon pero
|
||||
// con probabilidad mucho menor).
|
||||
// PINWHEEL: movement rectilini + boost de rotació visual prop del ship.
|
||||
void Enemy::behaviorPinwheel(float /*delta_time*/) {
|
||||
// Boost de rotación visual por proximidad al ship
|
||||
if (ship_position_ != nullptr) {
|
||||
const Vec2 TO_SHIP = *ship_position_ - center_;
|
||||
const float DIST = TO_SHIP.length();
|
||||
if (DIST < Defaults::Enemies::Pinwheel::PROXIMITY_DISTANCE) {
|
||||
rotation_delta_ = animation_.rotation_delta_base * Defaults::Enemies::Pinwheel::ROTATION_DELTA_PROXIMITY_MULTIPLIER;
|
||||
if (DIST < config_->behavior.proximity_distance) {
|
||||
rotation_delta_ = animation_.rotation_delta_base * config_->behavior.rotation_proximity_multiplier;
|
||||
} else {
|
||||
rotation_delta_ = animation_.rotation_delta_base;
|
||||
}
|
||||
}
|
||||
// Movimiento lineal puro: el world se encarga de integrar y rebotar.
|
||||
}
|
||||
|
||||
void Enemy::updateAnimation(float delta_time) {
|
||||
@@ -371,38 +305,26 @@ void Enemy::updateAnimation(float delta_time) {
|
||||
}
|
||||
|
||||
void Enemy::updatePulse(float delta_time) {
|
||||
const auto& cfg = config_->animation.pulse;
|
||||
if (animation_.pulse_active) {
|
||||
animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time;
|
||||
animation_.pulse_time_remaining -= delta_time;
|
||||
if (animation_.pulse_time_remaining <= 0.0F) {
|
||||
animation_.pulse_active = false;
|
||||
}
|
||||
} else {
|
||||
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 (RAND_VAL < TRIGGER_PROB) {
|
||||
animation_.pulse_active = true;
|
||||
animation_.pulse_phase = 0.0F;
|
||||
|
||||
const float FREQ_RANGE = Defaults::Enemies::Animation::PULSE_FREQ_MAX -
|
||||
Defaults::Enemies::Animation::PULSE_FREQ_MIN;
|
||||
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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (randFloat01() < cfg.trigger_prob_per_second * delta_time) {
|
||||
animation_.pulse_active = true;
|
||||
animation_.pulse_phase = 0.0F;
|
||||
animation_.pulse_frequency = randRange(cfg.frequency_min, cfg.frequency_max);
|
||||
animation_.pulse_amplitude = randRange(cfg.amplitude_min, cfg.amplitude_max);
|
||||
animation_.pulse_time_remaining = randRange(cfg.duration_min, cfg.duration_max);
|
||||
}
|
||||
}
|
||||
|
||||
void Enemy::updateRotationAcceleration(float delta_time) {
|
||||
const auto& cfg = config_->animation.rotation_accel;
|
||||
if (animation_.rotation_delta_t < 1.0F) {
|
||||
animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration;
|
||||
if (animation_.rotation_delta_t >= 1.0F) {
|
||||
@@ -416,34 +338,24 @@ void Enemy::updateRotationAcceleration(float delta_time) {
|
||||
const float TARGET = animation_.rotation_delta_target;
|
||||
rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
float scale = 1.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 SMOOTH_T = T * T * (3.0F - (2.0F * T));
|
||||
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START;
|
||||
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END;
|
||||
const float START = config_->spawn.invulnerability_scale_start;
|
||||
const float END = config_->spawn.invulnerability_scale_end;
|
||||
scale = START + ((END - START) * SMOOTH_T);
|
||||
} else if (animation_.pulse_active) {
|
||||
scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase);
|
||||
@@ -452,16 +364,7 @@ auto Enemy::computeCurrentScale() const -> float {
|
||||
}
|
||||
|
||||
auto Enemy::getBaseVelocity() const -> float {
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
return Defaults::Enemies::Pentagon::SPEED;
|
||||
case EnemyType::SQUARE:
|
||||
return Defaults::Enemies::Square::SPEED;
|
||||
case EnemyType::PINWHEEL:
|
||||
return Defaults::Enemies::Pinwheel::SPEED;
|
||||
default:
|
||||
return Defaults::Enemies::Pentagon::SPEED;
|
||||
}
|
||||
return EnemyRegistry::get(type_).physics.speed;
|
||||
}
|
||||
|
||||
auto Enemy::getBaseRotation() const -> float {
|
||||
@@ -474,12 +377,12 @@ void Enemy::setTrackingStrength(float strength) {
|
||||
}
|
||||
}
|
||||
|
||||
auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, 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 max_x;
|
||||
float min_y;
|
||||
float max_y;
|
||||
Constants::getSafePlayAreaBounds(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y);
|
||||
Constants::getSafePlayAreaBounds(collision_radius, min_x, max_x, min_y, max_y);
|
||||
|
||||
const int RANGE_X = static_cast<int>(max_x - min_x);
|
||||
const int RANGE_Y = static_cast<int>(max_y - min_y);
|
||||
@@ -489,5 +392,5 @@ auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -
|
||||
const float DX = out_x - ship_pos.x;
|
||||
const float DY = out_y - ship_pos.y;
|
||||
const float DISTANCE = std::sqrt((DX * DX) + (DY * DY));
|
||||
return DISTANCE >= Defaults::Enemies::Spawn::SAFETY_DISTANCE;
|
||||
return DISTANCE >= safety_distance;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/entities/entity.hpp"
|
||||
#include "core/types.hpp"
|
||||
|
||||
@@ -14,9 +13,13 @@
|
||||
enum class EnemyType : uint8_t {
|
||||
PENTAGON = 0, // Pentágono esquivador (zigzag)
|
||||
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.
|
||||
struct EnemyConfig;
|
||||
|
||||
// Estado de animación (palpitación + rotación acelerada)
|
||||
struct EnemyAnimation {
|
||||
// Palpitación (efecto respiración)
|
||||
@@ -48,10 +51,8 @@ class Enemy : public Entities::Entity {
|
||||
// Override: Interfaz de Entity
|
||||
[[nodiscard]] auto isActive() const -> bool override { return is_active_; }
|
||||
|
||||
// Override: Interfaz de colisión
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override {
|
||||
return Defaults::Entities::ENEMY_RADIUS;
|
||||
}
|
||||
// Override: Interfaz de colisión. El radi ve del config carregat per tipus.
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
|
||||
// Mentre fa spawn (invulnerable) segueix col·lisionant: les bales el
|
||||
// poden abatre i el cos físic rebota amb la nau. El damage a la nau
|
||||
// segueix filtrat per `isInvulnerable()` al detectShipEnemy.
|
||||
@@ -65,6 +66,9 @@ class Enemy : public Entities::Entity {
|
||||
// Getters
|
||||
[[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; }
|
||||
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
|
||||
// Configuració activa (carregada al darrer init()). Vàlida mentre l'enemic
|
||||
// ha estat inicialitzat almenys un cop; abans és nullptr.
|
||||
[[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; }
|
||||
|
||||
// Set ship position reference for tracking behavior
|
||||
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
|
||||
@@ -101,29 +105,34 @@ class Enemy : public Entities::Entity {
|
||||
void applyImpulse(const Vec2& impulse);
|
||||
|
||||
private:
|
||||
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
|
||||
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo
|
||||
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo.
|
||||
float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
|
||||
float rotation_{0.0F}; // Rotación visual acumulada (no afecta movimiento)
|
||||
// Configuració carregada per tipus (apunta a una entrada de EnemyRegistry).
|
||||
// nullptr abans del primer init(); per això getConfig() només és vàlid post-init.
|
||||
const EnemyConfig* config_{nullptr};
|
||||
|
||||
// Cache local del radi (per evitar dereferenciar config_ a getCollisionRadius);
|
||||
// s'actualitza a init() segons el tipus.
|
||||
float collision_radius_{0.0F};
|
||||
|
||||
float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s)
|
||||
float rotation_{0.0F}; // Rotación visual acumulada
|
||||
bool is_active_{false};
|
||||
|
||||
EnemyType type_{EnemyType::PENTAGON};
|
||||
EnemyAnimation animation_;
|
||||
|
||||
// Comportamiento type-specific
|
||||
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección
|
||||
const Vec2* ship_position_{nullptr}; // Puntero a posición de la nave (para tracking)
|
||||
float tracking_strength_{0.0F}; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5
|
||||
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección
|
||||
float tracking_timer_{0.0F};
|
||||
const Vec2* ship_position_{nullptr};
|
||||
float tracking_strength_{0.0F};
|
||||
float direction_change_timer_{0.0F};
|
||||
|
||||
// Invulnerabilidad post-spawn
|
||||
float invulnerability_timer_{0.0F};
|
||||
|
||||
// Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración.
|
||||
// Estado "herido"
|
||||
float wounded_timer_{0.0F};
|
||||
bool wound_expired_this_frame_{false};
|
||||
uint8_t last_hit_by_{0xFF}; // 0xFF = sin atribución
|
||||
uint8_t last_hit_by_{0xFF};
|
||||
|
||||
// Métodos privados
|
||||
void updateAnimation(float delta_time);
|
||||
@@ -133,8 +142,8 @@ class Enemy : public Entities::Entity {
|
||||
void behaviorSquare(float delta_time);
|
||||
void behaviorPinwheel(float delta_time);
|
||||
[[nodiscard]] auto computeCurrentScale() const -> float;
|
||||
// Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy.
|
||||
static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool;
|
||||
// Static: passa els paràmetres com a args per no acoblar a *this.
|
||||
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.
|
||||
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
// enemy_config.cpp - Implementació del parser de EnemyConfig
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "game/entities/enemy_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 aiTypeFromString(const std::string& s) -> std::optional<EnemyType> {
|
||||
if (s == "pentagon") { return EnemyType::PENTAGON; }
|
||||
if (s == "square") { return EnemyType::SQUARE; }
|
||||
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
|
||||
if (s == "star") { return EnemyType::STAR; }
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Cada parseXxx valida + omple la sub-struct corresponent. Retornen false
|
||||
// amb log si falta un camp requerit. Separar-los baixa la complexitat
|
||||
// cognitiva del fromYaml() principal.
|
||||
|
||||
auto parseAiType(const fkyaml::node& node, EnemyType expected, const std::string& name, EnemyType& out) -> bool {
|
||||
if (!node.contains("ai_type")) {
|
||||
std::cerr << "[EnemyConfig] Error: falta 'ai_type' a " << name << '\n';
|
||||
return false;
|
||||
}
|
||||
const auto AI_STR = node["ai_type"].get_value<std::string>();
|
||||
const auto PARSED = aiTypeFromString(AI_STR);
|
||||
if (!PARSED) {
|
||||
std::cerr << "[EnemyConfig] Error: ai_type desconegut '" << AI_STR << "' a " << name << '\n';
|
||||
return false;
|
||||
}
|
||||
if (*PARSED != expected) {
|
||||
std::cerr << "[EnemyConfig] Error: ai_type '" << AI_STR
|
||||
<< "' no coincideix amb el tipus esperat (per directori) a " << name << '\n';
|
||||
return false;
|
||||
}
|
||||
out = *PARSED;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto parseShape(const fkyaml::node& node, const std::string& name, EnemyConfig::ShapeCfg& out) -> bool {
|
||||
if (!node.contains("shape") || !node["shape"].contains("path")) {
|
||||
std::cerr << "[EnemyConfig] Error: falta 'shape.path' a " << name << '\n';
|
||||
return false;
|
||||
}
|
||||
const auto& shape = node["shape"];
|
||||
out.path = shape["path"].get_value<std::string>();
|
||||
out.scale = shape.contains("scale") ? shape["scale"].get_value<float>() : 1.0F;
|
||||
out.collision_factor = shape.contains("collision_factor")
|
||||
? shape["collision_factor"].get_value<float>()
|
||||
: 1.0F;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto parsePhysics(const fkyaml::node& node, const std::string& name, EnemyConfig::PhysicsCfg& out) -> bool {
|
||||
if (!node.contains("physics")) {
|
||||
std::cerr << "[EnemyConfig] Error: falta 'physics' a " << name << '\n';
|
||||
return false;
|
||||
}
|
||||
const auto& p = node["physics"];
|
||||
out.mass = p["mass"].get_value<float>();
|
||||
out.speed = p["speed"].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.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;
|
||||
}
|
||||
|
||||
// Tots els camps de behavior són opcionals; només l'AI corresponent els consumeix.
|
||||
void parseBehavior(const fkyaml::node& node, EnemyConfig::BehaviorCfg& out) {
|
||||
if (!node.contains("behavior")) {
|
||||
return;
|
||||
}
|
||||
const auto& b = node["behavior"];
|
||||
const auto READ_OPT = [&b](const char* key, float& dst) {
|
||||
if (b.contains(key)) {
|
||||
dst = b[key].get_value<float>();
|
||||
}
|
||||
};
|
||||
READ_OPT("zigzag_prob_per_second", out.zigzag_prob_per_second);
|
||||
READ_OPT("angle_change_max", out.angle_change_max);
|
||||
READ_OPT("tracking_strength", out.tracking_strength);
|
||||
READ_OPT("tracking_interval", out.tracking_interval);
|
||||
READ_OPT("rotation_proximity_multiplier", out.rotation_proximity_multiplier);
|
||||
READ_OPT("proximity_distance", out.proximity_distance);
|
||||
}
|
||||
|
||||
auto parseColors(const fkyaml::node& node, const std::string& name, EnemyConfig::ColorsCfg& out) -> bool {
|
||||
if (!node.contains("colors") ||
|
||||
!parseColor(node["colors"]["normal"], out.normal) ||
|
||||
!parseColor(node["colors"]["wounded"], out.wounded)) {
|
||||
std::cerr << "[EnemyConfig] Error: 'colors.normal' / 'colors.wounded' no són [r,g,b] a "
|
||||
<< name << '\n';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto parseScore(const fkyaml::node& node, const std::string& name, int& out) -> bool {
|
||||
if (!node.contains("score")) {
|
||||
std::cerr << "[EnemyConfig] Error: falta 'score' a " << name << '\n';
|
||||
return false;
|
||||
}
|
||||
out = node["score"].get_value<int>();
|
||||
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
|
||||
|
||||
auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
|
||||
-> std::optional<EnemyConfig> {
|
||||
try {
|
||||
EnemyConfig cfg;
|
||||
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "enemy";
|
||||
|
||||
if (!parseAiType(node, expected_ai_type, cfg.name, cfg.ai_type)) { return std::nullopt; }
|
||||
if (!parseShape(node, cfg.name, cfg.shape)) { return std::nullopt; }
|
||||
if (!parsePhysics(node, cfg.name, cfg.physics)) { return std::nullopt; }
|
||||
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 (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
|
||||
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
|
||||
|
||||
return cfg;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[EnemyConfig] Excepció parsejant: " << e.what() << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// enemy_config.hpp - Configuració d'un tipus d'enemic carregada des de YAML
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Una instància per tipus (Pentagon/Square/Pinwheel), carregada un cop al
|
||||
// startup per EnemyRegistry i compartida entre totes les instàncies d'aquell
|
||||
// tipus. Estructura paral·lela a PlayerConfig.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/enemy.hpp" // EnemyType
|
||||
#include "game/entities/enemy_event.hpp"
|
||||
|
||||
struct EnemyConfig {
|
||||
struct ShapeCfg {
|
||||
std::string path;
|
||||
float scale; // multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||
float collision_factor; // ajust opcional del hitbox respecte el cercle circumscrit (default 1.0)
|
||||
};
|
||||
|
||||
struct PhysicsCfg {
|
||||
float mass;
|
||||
float speed;
|
||||
float rotation_delta_min;
|
||||
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
|
||||
// i no s'usen — el dispatch viu a Enemy::behaviorXxx.
|
||||
struct BehaviorCfg {
|
||||
// Pentagon
|
||||
float zigzag_prob_per_second{0.0F};
|
||||
float angle_change_max{0.0F};
|
||||
// Square
|
||||
float tracking_strength{0.0F};
|
||||
float tracking_interval{0.0F};
|
||||
// Pinwheel
|
||||
float rotation_proximity_multiplier{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 {
|
||||
SDL_Color normal;
|
||||
SDL_Color wounded;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
EnemyType ai_type;
|
||||
ShapeCfg shape;
|
||||
PhysicsCfg physics;
|
||||
BehaviorCfg behavior;
|
||||
AnimationCfg animation;
|
||||
WoundedCfg wounded;
|
||||
SpawnCfg spawn;
|
||||
ColorsCfg colors;
|
||||
int score;
|
||||
EnemyEventConfig events;
|
||||
|
||||
// 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).
|
||||
static auto fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
|
||||
-> std::optional<EnemyConfig>;
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// enemy_registry.cpp - Implementació del registre estàtic d'enemics
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "game/entities/enemy_registry.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/entities/entity_loader.hpp"
|
||||
|
||||
EnemyConfig EnemyRegistry::pentagon_config;
|
||||
EnemyConfig EnemyRegistry::square_config;
|
||||
EnemyConfig EnemyRegistry::pinwheel_config;
|
||||
EnemyConfig EnemyRegistry::star_config;
|
||||
bool EnemyRegistry::loaded = false;
|
||||
|
||||
namespace {
|
||||
|
||||
auto loadOne(const std::string& name, EnemyType expected_type, EnemyConfig& out) -> bool {
|
||||
auto yaml = Entities::EntityLoader::load(name);
|
||||
if (!yaml) {
|
||||
std::cerr << "[EnemyRegistry] Error: no s'ha pogut carregar " << name << ".yaml\n";
|
||||
return false;
|
||||
}
|
||||
auto cfg = EnemyConfig::fromYaml(*yaml, expected_type);
|
||||
if (!cfg) {
|
||||
std::cerr << "[EnemyRegistry] Error: format invàlid a " << name << ".yaml\n";
|
||||
return false;
|
||||
}
|
||||
out = *cfg;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto EnemyRegistry::loadAll() -> bool {
|
||||
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
|
||||
loadOne("square", EnemyType::SQUARE, square_config) &&
|
||||
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config) &&
|
||||
loadOne("star", EnemyType::STAR, star_config);
|
||||
loaded = OK;
|
||||
if (OK) {
|
||||
std::cout << "[EnemyRegistry] 4 configuracions d'enemic carregades.\n";
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
auto EnemyRegistry::get(EnemyType type) -> const EnemyConfig& {
|
||||
if (!loaded) {
|
||||
std::cerr << "[EnemyRegistry] FATAL: get() abans de loadAll()\n";
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
switch (type) {
|
||||
case EnemyType::PENTAGON:
|
||||
return pentagon_config;
|
||||
case EnemyType::SQUARE:
|
||||
return square_config;
|
||||
case EnemyType::PINWHEEL:
|
||||
return pinwheel_config;
|
||||
case EnemyType::STAR:
|
||||
return star_config;
|
||||
}
|
||||
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// enemy_registry.hpp - Registre estàtic de configuracions d'enemics per tipus
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Carrega els 3 fitxers YAML (pentagon, square, pinwheel) un cop al startup
|
||||
// i exposa el lookup per EnemyType. Pensat per a ser invocat des de
|
||||
// GameScene; si la càrrega falla, el caller decideix avortar.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "game/entities/enemy.hpp"
|
||||
#include "game/entities/enemy_config.hpp"
|
||||
|
||||
class EnemyRegistry {
|
||||
public:
|
||||
EnemyRegistry() = delete; // tot estàtic
|
||||
|
||||
// Carrega els 3 descriptors. Retorna true si tots tres s'han carregat
|
||||
// i parsejat correctament. Cridar abans del primer get().
|
||||
static auto loadAll() -> bool;
|
||||
|
||||
// Lookup. Cal haver cridat loadAll() abans. Si el tipus no s'ha carregat
|
||||
// (loadAll fallida o no cridada), avorta amb log fatal.
|
||||
static auto get(EnemyType type) -> const EnemyConfig&;
|
||||
|
||||
private:
|
||||
static EnemyConfig pentagon_config;
|
||||
static EnemyConfig square_config;
|
||||
static EnemyConfig pinwheel_config;
|
||||
static EnemyConfig star_config;
|
||||
static bool loaded;
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
// player_config.cpp - Implementació del parser de PlayerConfig
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "game/entities/player_config.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
// Helper: extreu un SDL_Color d'una seqüència de 3 enters [r, g, b] del YAML.
|
||||
// Retorna true si el format és vàlid.
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto PlayerConfig::fromYaml(const fkyaml::node& node) -> std::optional<PlayerConfig> {
|
||||
try {
|
||||
PlayerConfig cfg;
|
||||
|
||||
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "player";
|
||||
|
||||
// shape
|
||||
if (!node.contains("shape") || !node["shape"].contains("path")) {
|
||||
std::cerr << "[PlayerConfig] Error: falta 'shape.path'" << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto& shape = node["shape"];
|
||||
cfg.shape.path = shape["path"].get_value<std::string>();
|
||||
cfg.shape.scale = shape.contains("scale") ? shape["scale"].get_value<float>() : 1.0F;
|
||||
cfg.shape.collision_factor = shape.contains("collision_factor")
|
||||
? shape["collision_factor"].get_value<float>()
|
||||
: 1.0F;
|
||||
|
||||
// physics
|
||||
if (!node.contains("physics")) {
|
||||
std::cerr << "[PlayerConfig] Error: falta 'physics'" << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto& physics = node["physics"];
|
||||
cfg.physics.mass = physics["mass"].get_value<float>();
|
||||
cfg.physics.restitution = physics["restitution"].get_value<float>();
|
||||
cfg.physics.linear_damping = physics["linear_damping"].get_value<float>();
|
||||
cfg.physics.angular_damping = physics["angular_damping"].get_value<float>();
|
||||
cfg.physics.rotation_speed = physics["rotation_speed"].get_value<float>();
|
||||
cfg.physics.acceleration = physics["acceleration"].get_value<float>();
|
||||
cfg.physics.max_velocity = physics["max_velocity"].get_value<float>();
|
||||
cfg.physics.death_impact_factor = physics["death_impact_factor"].get_value<float>();
|
||||
|
||||
// invulnerability
|
||||
if (!node.contains("invulnerability")) {
|
||||
std::cerr << "[PlayerConfig] Error: falta 'invulnerability'" << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto& invul = node["invulnerability"];
|
||||
cfg.invulnerability.duration = invul["duration"].get_value<float>();
|
||||
cfg.invulnerability.blink_visible = invul["blink_visible"].get_value<float>();
|
||||
cfg.invulnerability.blink_invisible = invul["blink_invisible"].get_value<float>();
|
||||
|
||||
// hurt
|
||||
if (!node.contains("hurt")) {
|
||||
std::cerr << "[PlayerConfig] Error: falta 'hurt'" << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
cfg.hurt.duration = node["hurt"]["duration"].get_value<float>();
|
||||
cfg.hurt.blink_hz = node["hurt"]["blink_hz"].get_value<float>();
|
||||
|
||||
// visual_thrust
|
||||
if (!node.contains("visual_thrust")) {
|
||||
std::cerr << "[PlayerConfig] Error: falta 'visual_thrust'" << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
cfg.visual_thrust.push_divisor = node["visual_thrust"]["push_divisor"].get_value<float>();
|
||||
cfg.visual_thrust.scale_divisor = node["visual_thrust"]["scale_divisor"].get_value<float>();
|
||||
|
||||
// colors
|
||||
if (!node.contains("colors") ||
|
||||
!parseColor(node["colors"]["normal"], cfg.colors.normal) ||
|
||||
!parseColor(node["colors"]["hurt"], cfg.colors.hurt)) {
|
||||
std::cerr << "[PlayerConfig] Error: 'colors.normal' / 'colors.hurt' no són seqüències [r,g,b]" << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// weapon
|
||||
if (!node.contains("weapon")) {
|
||||
std::cerr << "[PlayerConfig] Error: falta 'weapon'" << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
cfg.weapon.bullet_speed = node["weapon"]["bullet_speed"].get_value<float>();
|
||||
|
||||
return cfg;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[PlayerConfig] Excepció parsejant: " << e.what() << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// player_config.hpp - Configuració de la nau del player carregada des de YAML
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// POD struct amb sub-structs per organitzar els paràmetres del jugador
|
||||
// (física, invulnerabilitat, hurt, empenta visual, colors, weapon). Es
|
||||
// construeix a partir d'un node fkyaml carregat per EntityLoader.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
struct PlayerConfig {
|
||||
struct ShapeCfg {
|
||||
std::string path;
|
||||
float scale; // multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||
float collision_factor; // ajust opcional del hitbox respecte el cercle circumscrit (default 1.0)
|
||||
};
|
||||
|
||||
struct PhysicsCfg {
|
||||
float mass;
|
||||
float restitution;
|
||||
float linear_damping;
|
||||
float angular_damping;
|
||||
float rotation_speed; // rad/s
|
||||
float acceleration; // px/s^2 multiplicat per la massa
|
||||
float max_velocity; // px/s (clamp post-integració)
|
||||
float death_impact_factor; // [0..1] moment transferit a l'enemic al morir
|
||||
};
|
||||
|
||||
struct InvulnerabilityCfg {
|
||||
float duration;
|
||||
float blink_visible;
|
||||
float blink_invisible;
|
||||
};
|
||||
|
||||
struct HurtCfg {
|
||||
float duration;
|
||||
float blink_hz;
|
||||
};
|
||||
|
||||
struct VisualThrustCfg {
|
||||
float push_divisor;
|
||||
float scale_divisor;
|
||||
};
|
||||
|
||||
struct ColorsCfg {
|
||||
SDL_Color normal;
|
||||
SDL_Color hurt;
|
||||
};
|
||||
|
||||
struct WeaponCfg {
|
||||
float bullet_speed;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
ShapeCfg shape;
|
||||
PhysicsCfg physics;
|
||||
InvulnerabilityCfg invulnerability;
|
||||
HurtCfg hurt;
|
||||
VisualThrustCfg visual_thrust;
|
||||
ColorsCfg colors;
|
||||
WeaponCfg weapon;
|
||||
|
||||
// Construeix un PlayerConfig a partir del node YAML. Retorna std::nullopt
|
||||
// si falten camps requerits o el format no és vàlid (el caller decideix
|
||||
// si abortar).
|
||||
static auto fromYaml(const fkyaml::node& node) -> std::optional<PlayerConfig>;
|
||||
};
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/defaults.hpp"
|
||||
@@ -20,27 +21,30 @@
|
||||
#include "core/types.hpp"
|
||||
#include "game/constants.hpp"
|
||||
|
||||
Ship::Ship(Rendering::Renderer* renderer, const char* shape_file)
|
||||
: Entity(renderer) {
|
||||
// Brightness específico para naves
|
||||
Ship::Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape_override)
|
||||
: Entity(renderer),
|
||||
config_(std::move(config)) {
|
||||
brightness_ = Defaults::Brightness::NAU;
|
||||
|
||||
// Configuración del cuerpo físico
|
||||
body_.setMass(Defaults::Ship::MASS);
|
||||
body_.radius = Defaults::Entities::SHIP_RADIUS;
|
||||
body_.restitution = Defaults::Ship::RESTITUTION;
|
||||
body_.linear_damping = Defaults::Ship::LINEAR_DAMPING;
|
||||
body_.angular_damping = Defaults::Ship::ANGULAR_DAMPING;
|
||||
|
||||
// Cargar shape compartida desde archivo
|
||||
shape_ = Graphics::ShapeLoader::load(shape_file);
|
||||
// El shape pot venir del YAML o ser overridden (ex: P2 amb "ship2.shp").
|
||||
const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path;
|
||||
shape_ = Graphics::ShapeLoader::load(SHAPE_PATH);
|
||||
if (!shape_ || !shape_->isValid()) {
|
||||
std::cerr << "[Ship] Error: no se ha podido cargar " << shape_file << '\n';
|
||||
std::cerr << "[Ship] Error: no se ha podido cargar " << SHAPE_PATH << '\n';
|
||||
}
|
||||
|
||||
// Radi de col·lisió derivat del cercle circumscrit de la shape * scale * collision_factor.
|
||||
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
|
||||
collision_radius_ = BOUNDING * config_.shape.scale * config_.shape.collision_factor;
|
||||
|
||||
body_.setMass(config_.physics.mass);
|
||||
body_.radius = collision_radius_;
|
||||
body_.restitution = config_.physics.restitution;
|
||||
body_.linear_damping = config_.physics.linear_damping;
|
||||
body_.angular_damping = config_.physics.angular_damping;
|
||||
}
|
||||
|
||||
void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) {
|
||||
// Posición inicial
|
||||
if (spawn_point != nullptr) {
|
||||
center_ = *spawn_point;
|
||||
} else {
|
||||
@@ -50,34 +54,27 @@ void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) {
|
||||
center_ = {.x = center_x, .y = center_y};
|
||||
}
|
||||
|
||||
// Reset orientación
|
||||
angle_ = 0.0F;
|
||||
|
||||
// Sincronizar cuerpo físico con la posición/orientación inicial
|
||||
body_.position = center_;
|
||||
body_.angle = angle_;
|
||||
body_.velocity = Vec2{};
|
||||
body_.angular_velocity = 0.0F;
|
||||
body_.clearAccumulators();
|
||||
|
||||
// Activar invulnerabilidad solo si es respawn
|
||||
invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F;
|
||||
invulnerable_timer_ = activar_invulnerabilitat ? config_.invulnerability.duration : 0.0F;
|
||||
is_hit_ = false;
|
||||
hurt_timer_ = 0.0F;
|
||||
touching_enemy_prev_frame_ = false;
|
||||
}
|
||||
|
||||
void Ship::processInput(float delta_time, uint8_t player_id) {
|
||||
// Solo procesa input si la nave está viva
|
||||
if (is_hit_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* input = Input::get();
|
||||
|
||||
// Rotación: control directo del ángulo (no física, no inercial).
|
||||
// Se actualiza también body_.angle para que el dibujado tras
|
||||
// postUpdate refleje el cambio inmediatamente.
|
||||
const bool ROTATE_RIGHT = (player_id == 0)
|
||||
? input->checkActionPlayer1(InputAction::RIGHT, Input::ALLOW_REPEAT)
|
||||
: input->checkActionPlayer2(InputAction::RIGHT, Input::ALLOW_REPEAT);
|
||||
@@ -89,10 +86,10 @@ void Ship::processInput(float delta_time, uint8_t player_id) {
|
||||
: input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT);
|
||||
|
||||
if (ROTATE_RIGHT) {
|
||||
body_.angle += Defaults::Physics::ROTATION_SPEED * delta_time;
|
||||
body_.angle += config_.physics.rotation_speed * delta_time;
|
||||
}
|
||||
if (ROTATE_LEFT) {
|
||||
body_.angle -= Defaults::Physics::ROTATION_SPEED * delta_time;
|
||||
body_.angle -= config_.physics.rotation_speed * delta_time;
|
||||
}
|
||||
|
||||
// Thrust: fuerza vectorial en la dirección de la nariz.
|
||||
@@ -100,44 +97,36 @@ void Ship::processInput(float delta_time, uint8_t player_id) {
|
||||
if (THRUST) {
|
||||
const float DIR_X = std::cos(body_.angle - (Constants::PI / 2.0F));
|
||||
const float DIR_Y = std::sin(body_.angle - (Constants::PI / 2.0F));
|
||||
// Fuerza = masa * aceleración: 10 kg * 400 px/s² = 4000 (unidades arcade)
|
||||
const float MAGNITUDE = body_.mass * Defaults::Physics::ACCELERATION;
|
||||
const float MAGNITUDE = body_.mass * config_.physics.acceleration;
|
||||
body_.applyForce(Vec2{.x = DIR_X * MAGNITUDE, .y = DIR_Y * MAGNITUDE});
|
||||
}
|
||||
}
|
||||
|
||||
void Ship::update(float delta_time) {
|
||||
// Solo update si la nave está viva
|
||||
if (is_hit_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrementar timer de invulnerabilidad
|
||||
if (invulnerable_timer_ > 0.0F) {
|
||||
invulnerable_timer_ -= delta_time;
|
||||
invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F);
|
||||
}
|
||||
|
||||
// Decrementar timer d'estat HURT (a 0 → torna a normal sense efecte extern)
|
||||
if (hurt_timer_ > 0.0F) {
|
||||
hurt_timer_ -= delta_time;
|
||||
hurt_timer_ = std::max(hurt_timer_, 0.0F);
|
||||
}
|
||||
|
||||
// El movimiento real lo hace PhysicsWorld::update().
|
||||
// Aquí solo lógica de estado.
|
||||
|
||||
// Cap de velocidad: el thrust acumula fuerza sin límite; limitamos
|
||||
// la magnitud de body_.velocity tras aplicar fuerzas para preservar
|
||||
// el feel arcade del MAX_VELOCITY original.
|
||||
const float CURRENT_SPEED = body_.velocity.length();
|
||||
if (CURRENT_SPEED > Defaults::Physics::MAX_VELOCITY) {
|
||||
body_.velocity = body_.velocity * (Defaults::Physics::MAX_VELOCITY / CURRENT_SPEED);
|
||||
if (CURRENT_SPEED > config_.physics.max_velocity) {
|
||||
body_.velocity = body_.velocity * (config_.physics.max_velocity / CURRENT_SPEED);
|
||||
}
|
||||
}
|
||||
|
||||
void Ship::postUpdate(float /*delta_time*/) {
|
||||
// Sincronizar mirror desde body_ tras la integración del world.
|
||||
center_ = body_.position;
|
||||
angle_ = body_.angle;
|
||||
}
|
||||
@@ -147,11 +136,10 @@ void Ship::draw() const {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parpadeo si invulnerable
|
||||
if (isInvulnerable()) {
|
||||
const float BLINK_CYCLE = Defaults::Ship::BLINK_VISIBLE_TIME + Defaults::Ship::BLINK_INVISIBLE_TIME;
|
||||
const float BLINK_CYCLE = config_.invulnerability.blink_visible + config_.invulnerability.blink_invisible;
|
||||
const float TIME_IN_CYCLE = std::fmod(invulnerable_timer_, BLINK_CYCLE);
|
||||
if (TIME_IN_CYCLE < Defaults::Ship::BLINK_INVISIBLE_TIME) {
|
||||
if (TIME_IN_CYCLE < config_.invulnerability.blink_invisible) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -160,20 +148,19 @@ void Ship::draw() const {
|
||||
return;
|
||||
}
|
||||
|
||||
// Efecto visual de empuje: escala proporcional a la velocidad.
|
||||
// 0..200 px/s → escala 1.0..1.5 (manteniendo la sensación del Pascal original).
|
||||
// Efecte visual d'empenta (modulador sobre l'escala base del YAML).
|
||||
const float SPEED = getSpeed();
|
||||
const float VISUAL_PUSH = SPEED / Defaults::Ship::VISUAL_PUSH_DIVISOR;
|
||||
const float SCALE = 1.0F + (VISUAL_PUSH / Defaults::Ship::VISUAL_SCALE_DIVISOR);
|
||||
const float VISUAL_PUSH = SPEED / config_.visual_thrust.push_divisor;
|
||||
const float THRUST_MODULATOR = 1.0F + (VISUAL_PUSH / config_.visual_thrust.scale_divisor);
|
||||
const float SCALE = config_.shape.scale * THRUST_MODULATOR;
|
||||
|
||||
// Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt
|
||||
// a Hurt::BLINK_HZ (mateixa estètica que el wounded dels enemics).
|
||||
SDL_Color color = color_normal_;
|
||||
// Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt.
|
||||
SDL_Color color = config_.colors.normal;
|
||||
if (hurt_timer_ > 0.0F) {
|
||||
const float CYCLE = 1.0F / Defaults::Ship::Hurt::BLINK_HZ;
|
||||
const float CYCLE = 1.0F / config_.hurt.blink_hz;
|
||||
const float T = std::fmod(hurt_timer_, CYCLE);
|
||||
if (T < (CYCLE / 2.0F)) {
|
||||
color = color_hurt_;
|
||||
color = config_.colors.hurt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +168,6 @@ void Ship::draw() const {
|
||||
}
|
||||
|
||||
void Ship::hurt() {
|
||||
hurt_timer_ = Defaults::Ship::Hurt::DURATION;
|
||||
hurt_timer_ = config_.hurt.duration;
|
||||
Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME);
|
||||
}
|
||||
|
||||
@@ -6,15 +6,18 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/entities/entity.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/entities/player_config.hpp"
|
||||
|
||||
class Ship : public Entities::Entity {
|
||||
public:
|
||||
Ship()
|
||||
: Entity(nullptr) {}
|
||||
explicit Ship(Rendering::Renderer* renderer, const char* shape_file = "ship.shp");
|
||||
// shape_override: si no és nullptr, substitueix config.shape.path
|
||||
// (utilitzat per donar al P2 un model visual diferent compartint la
|
||||
// mateixa configuració del player).
|
||||
explicit Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape_override = nullptr);
|
||||
|
||||
void init() override { init(nullptr, false); }
|
||||
void init(const Vec2* spawn_point, bool activar_invulnerabilitat = false);
|
||||
@@ -26,20 +29,18 @@ class Ship : public Entities::Entity {
|
||||
// Override: Interfaz de Entity
|
||||
[[nodiscard]] auto isActive() const -> bool override { return !is_hit_; }
|
||||
|
||||
// Override: Interfaz de colisión
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override {
|
||||
return Defaults::Entities::SHIP_RADIUS;
|
||||
}
|
||||
// Override: Interfaz de colisión. Derivat al ctor del bounding_radius del
|
||||
// shape carregat × scale × collision_factor.
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
|
||||
[[nodiscard]] auto isCollidable() const -> bool override {
|
||||
return !is_hit_ && invulnerable_timer_ <= 0.0F;
|
||||
}
|
||||
|
||||
// Getters
|
||||
[[nodiscard]] auto isInvulnerable() const -> bool { return invulnerable_timer_ > 0.0F; }
|
||||
// Velocidad como vector cartesiano (ahora viene directa del body_).
|
||||
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
|
||||
// Velocidad escalar (utilidad para draw y debugging).
|
||||
[[nodiscard]] auto getSpeed() const -> float { return body_.velocity.length(); }
|
||||
[[nodiscard]] auto getConfig() const -> const PlayerConfig& { return config_; }
|
||||
|
||||
// Setters
|
||||
void setCenter(const Vec2& nou_centre) {
|
||||
@@ -65,17 +66,18 @@ class Ship : public Entities::Entity {
|
||||
void setTouchingEnemyPrevFrame(bool touching) { touching_enemy_prev_frame_ = touching; }
|
||||
|
||||
private:
|
||||
// Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
|
||||
// Inicializados en la declaración: el ctor por defecto deja la nave "viva y sin invulnerabilidad",
|
||||
// que es el estado coherente al que llevan tanto init() como el ctor con renderer.
|
||||
// Configuració carregada des de YAML. Default-init zero permet el ctor
|
||||
// per defecte (necessari per a `std::array<Ship, N>`); s'omple via
|
||||
// copy/move-assignment quan GameScene crea la nau real.
|
||||
PlayerConfig config_{};
|
||||
|
||||
// Radi de col·lisió derivat: shape.bounding_radius × shape.scale × shape.collision_factor.
|
||||
float collision_radius_{0.0F};
|
||||
|
||||
bool is_hit_{false};
|
||||
float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable
|
||||
|
||||
// Colors de la nau (propietats, prep per migració a YAML).
|
||||
SDL_Color color_normal_{Defaults::Palette::SHIP};
|
||||
SDL_Color color_hurt_{Defaults::Palette::WOUNDED};
|
||||
|
||||
// >0 → estat HURT (parpelleig color_normal_ ↔ color_hurt_).
|
||||
// >0 → estat HURT (parpelleig color normal ↔ color hurt).
|
||||
float hurt_timer_{0.0F};
|
||||
|
||||
// Edge-trigger: true si el frame anterior la nau ja estava en contacte amb un enemic.
|
||||
|
||||
@@ -10,10 +10,14 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/entities/entity_loader.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/system/scene_context.hpp"
|
||||
#include "core/system/service_menu.hpp"
|
||||
#include "game/entities/bullet_registry.hpp"
|
||||
#include "game/entities/enemy_registry.hpp"
|
||||
#include "game/entities/player_config.hpp"
|
||||
#include "game/stage_system/stage_loader.hpp"
|
||||
#include "game/systems/collision_system.hpp"
|
||||
#include "game/systems/continue_system.hpp"
|
||||
@@ -49,9 +53,36 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
||||
auto option = context_.consumeOption();
|
||||
(void)option; // Suprimir warning de variable no usada
|
||||
|
||||
// Inicialitzar naves con renderer (P1=ship.shp, P2=ship2.shp)
|
||||
ships_[0] = Ship(sdl.getRenderer(), "ship.shp"); // Jugador 1: nave estàndar
|
||||
ships_[1] = Ship(sdl.getRenderer(), "ship2.shp"); // Jugador 2: interceptor con ales
|
||||
// Carregar la configuració del player des de YAML. Sense fallback: si
|
||||
// falla, abortem (la nau no és construïble sense paràmetres).
|
||||
auto player_yaml = Entities::EntityLoader::load("player");
|
||||
if (!player_yaml) {
|
||||
std::cerr << "[GameScene] FATAL: no s'ha pogut carregar data/entities/player/player.yaml\n";
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
auto player_config = PlayerConfig::fromYaml(*player_yaml);
|
||||
if (!player_config) {
|
||||
std::cerr << "[GameScene] FATAL: player.yaml mal format\n";
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// Carregar les configuracions dels 3 enemics. Sense fallback: si falla,
|
||||
// abortem (els enemics no es poden construir sense els seus paràmetres).
|
||||
if (!EnemyRegistry::loadAll()) {
|
||||
std::cerr << "[GameScene] FATAL: no s'han pogut carregar els enemics YAML\n";
|
||||
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.
|
||||
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
|
||||
|
||||
// Inicialitzar balas con renderer
|
||||
std::ranges::fill(bullets_, Bullet(sdl.getRenderer()));
|
||||
@@ -758,7 +789,7 @@ void GameScene::tocado(uint8_t player_id) {
|
||||
0.0F, // sense herència angular
|
||||
0.0F, // sin herencia visual
|
||||
Defaults::Sound::EXPLOSION2,
|
||||
Defaults::Palette::SHIP,
|
||||
ships_[player_id].getConfig().colors.normal,
|
||||
Defaults::Physics::Debris::ENEMY_LIFETIME,
|
||||
Defaults::Physics::Debris::ENEMY_FRICTION,
|
||||
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
|
||||
@@ -944,7 +975,7 @@ void GameScene::fireBullet(uint8_t player_id) {
|
||||
const int START_IDX = player_id * SLOTS_PER_PLAYER;
|
||||
for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) {
|
||||
if (!bullets_[i].isActive()) {
|
||||
bullets_[i].fire(fire_position, ship_angle, player_id);
|
||||
bullets_[i].fire(fire_position, ship_angle, player_id, ships_[player_id].getConfig().weapon.bullet_speed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,10 @@ namespace StageSystem {
|
||||
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
|
||||
return EnemyType::SQUARE;
|
||||
}
|
||||
return EnemyType::PINWHEEL;
|
||||
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado + config_->distribucio.molinillo) {
|
||||
return EnemyType::PINWHEEL;
|
||||
}
|
||||
return EnemyType::STAR;
|
||||
}
|
||||
|
||||
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace StageSystem {
|
||||
uint8_t pentagon; // 0-100
|
||||
uint8_t cuadrado; // 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
|
||||
};
|
||||
|
||||
@@ -59,7 +60,7 @@ namespace StageSystem {
|
||||
// el tipo; basta con confirmar que no es 0 (sentinela "sin asignar").
|
||||
return stage_id >= 1 &&
|
||||
total_enemies > 0 && total_enemies <= 200 &&
|
||||
distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo == 100;
|
||||
distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo + distribucio.star == 100;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,241 +19,243 @@
|
||||
|
||||
namespace StageSystem {
|
||||
|
||||
auto StageLoader::load(const std::string& path) -> std::unique_ptr<StageSystemConfig> {
|
||||
try {
|
||||
// Normalize path: "data/stages/stages.yaml" → "stages/stages.yaml"
|
||||
std::string normalized = path;
|
||||
if (normalized.starts_with("data/")) {
|
||||
normalized = normalized.substr(5);
|
||||
}
|
||||
auto StageLoader::load(const std::string& path) -> std::unique_ptr<StageSystemConfig> {
|
||||
try {
|
||||
// Normalize path: "data/stages/stages.yaml" → "stages/stages.yaml"
|
||||
std::string normalized = path;
|
||||
if (normalized.starts_with("data/")) {
|
||||
normalized = normalized.substr(5);
|
||||
}
|
||||
|
||||
// Load from resource system
|
||||
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
||||
if (data.empty()) {
|
||||
std::cerr << "[StageLoader] Error: no es pot load " << normalized << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Convert to string
|
||||
std::string yaml_content(data.begin(), data.end());
|
||||
std::stringstream stream(yaml_content);
|
||||
|
||||
// Parse YAML
|
||||
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
||||
auto config = std::make_unique<StageSystemConfig>();
|
||||
|
||||
// Parse metadata
|
||||
if (!yaml.contains("metadata")) {
|
||||
std::cerr << "[StageLoader] Error: falta camp 'metadata'" << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
if (!parseMetadata(yaml["metadata"], config->metadata)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Parse stages
|
||||
if (!yaml.contains("stages")) {
|
||||
std::cerr << "[StageLoader] Error: falta camp 'stages'" << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!yaml["stages"].is_sequence()) {
|
||||
std::cerr << "[StageLoader] Error: 'stages' ha de ser una list" << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (const auto& stage_yaml : yaml["stages"]) {
|
||||
StageConfig stage;
|
||||
if (!parseStage(stage_yaml, stage)) {
|
||||
// Load from resource system
|
||||
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
||||
if (data.empty()) {
|
||||
std::cerr << "[StageLoader] Error: no es pot load " << normalized << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
config->stages.push_back(stage);
|
||||
}
|
||||
|
||||
// Validar configuración
|
||||
if (!validateConfig(*config)) {
|
||||
// Convert to string
|
||||
std::string yaml_content(data.begin(), data.end());
|
||||
std::stringstream stream(yaml_content);
|
||||
|
||||
// Parse YAML
|
||||
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
||||
auto config = std::make_unique<StageSystemConfig>();
|
||||
|
||||
// Parse metadata
|
||||
if (!yaml.contains("metadata")) {
|
||||
std::cerr << "[StageLoader] Error: falta camp 'metadata'" << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
if (!parseMetadata(yaml["metadata"], config->metadata)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Parse stages
|
||||
if (!yaml.contains("stages")) {
|
||||
std::cerr << "[StageLoader] Error: falta camp 'stages'" << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!yaml["stages"].is_sequence()) {
|
||||
std::cerr << "[StageLoader] Error: 'stages' ha de ser una list" << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (const auto& stage_yaml : yaml["stages"]) {
|
||||
StageConfig stage;
|
||||
if (!parseStage(stage_yaml, stage)) {
|
||||
return nullptr;
|
||||
}
|
||||
config->stages.push_back(stage);
|
||||
}
|
||||
|
||||
// Validar configuración
|
||||
if (!validateConfig(*config)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::cout << "[StageLoader] Carregats " << config->stages.size()
|
||||
<< " stages correctament" << '\n';
|
||||
return config;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Excepció: " << e.what() << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::cout << "[StageLoader] Carregats " << config->stages.size()
|
||||
<< " stages correctament" << '\n';
|
||||
return config;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Excepció: " << e.what() << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
auto StageLoader::parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool {
|
||||
try {
|
||||
if (!yaml.contains("version") || !yaml.contains("total_stages")) {
|
||||
std::cerr << "[StageLoader] Error: metadata incompleta" << '\n';
|
||||
auto StageLoader::parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool {
|
||||
try {
|
||||
if (!yaml.contains("version") || !yaml.contains("total_stages")) {
|
||||
std::cerr << "[StageLoader] Error: metadata incompleta" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
meta.version = yaml["version"].get_value<std::string>();
|
||||
meta.total_stages = yaml["total_stages"].get_value<uint8_t>();
|
||||
meta.descripcio = yaml.contains("description")
|
||||
? yaml["description"].get_value<std::string>()
|
||||
: "";
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Error parsing metadata: " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
meta.version = yaml["version"].get_value<std::string>();
|
||||
meta.total_stages = yaml["total_stages"].get_value<uint8_t>();
|
||||
meta.descripcio = yaml.contains("description")
|
||||
? yaml["description"].get_value<std::string>()
|
||||
: "";
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Error parsing metadata: " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool {
|
||||
try {
|
||||
if (!yaml.contains("stage_id") || !yaml.contains("total_enemies") ||
|
||||
!yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") ||
|
||||
!yaml.contains("difficulty_multipliers")) {
|
||||
std::cerr << "[StageLoader] Error: stage incompleta" << '\n';
|
||||
auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool {
|
||||
try {
|
||||
if (!yaml.contains("stage_id") || !yaml.contains("total_enemies") ||
|
||||
!yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") ||
|
||||
!yaml.contains("difficulty_multipliers")) {
|
||||
std::cerr << "[StageLoader] Error: stage incompleta" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
stage.stage_id = yaml["stage_id"].get_value<uint8_t>();
|
||||
stage.total_enemies = yaml["total_enemies"].get_value<uint8_t>();
|
||||
|
||||
if (!parseSpawnConfig(yaml["spawn_config"], stage.config_spawn)) {
|
||||
return false;
|
||||
}
|
||||
if (!parseDistribution(yaml["enemy_distribution"], stage.distribucio)) {
|
||||
return false;
|
||||
}
|
||||
if (!parseMultipliers(yaml["difficulty_multipliers"], stage.multiplicadors)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!stage.isValid()) {
|
||||
std::cerr << "[StageLoader] Error: stage " << static_cast<int>(stage.stage_id)
|
||||
<< " no es vàlid" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Error parsing stage: " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
stage.stage_id = yaml["stage_id"].get_value<uint8_t>();
|
||||
stage.total_enemies = yaml["total_enemies"].get_value<uint8_t>();
|
||||
|
||||
if (!parseSpawnConfig(yaml["spawn_config"], stage.config_spawn)) {
|
||||
return false;
|
||||
}
|
||||
if (!parseDistribution(yaml["enemy_distribution"], stage.distribucio)) {
|
||||
return false;
|
||||
}
|
||||
if (!parseMultipliers(yaml["difficulty_multipliers"], stage.multiplicadors)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!stage.isValid()) {
|
||||
std::cerr << "[StageLoader] Error: stage " << static_cast<int>(stage.stage_id)
|
||||
<< " no es vàlid" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Error parsing stage: " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool {
|
||||
try {
|
||||
if (!yaml.contains("mode") || !yaml.contains("initial_delay") ||
|
||||
!yaml.contains("spawn_interval")) {
|
||||
std::cerr << "[StageLoader] Error: spawn_config incompleta" << '\n';
|
||||
auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool {
|
||||
try {
|
||||
if (!yaml.contains("mode") || !yaml.contains("initial_delay") ||
|
||||
!yaml.contains("spawn_interval")) {
|
||||
std::cerr << "[StageLoader] Error: spawn_config incompleta" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
auto mode_str = yaml["mode"].get_value<std::string>();
|
||||
config.mode = parseSpawnMode(mode_str);
|
||||
config.delay_inicial = yaml["initial_delay"].get_value<float>();
|
||||
config.interval_spawn = yaml["spawn_interval"].get_value<float>();
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
auto mode_str = yaml["mode"].get_value<std::string>();
|
||||
config.mode = parseSpawnMode(mode_str);
|
||||
config.delay_inicial = yaml["initial_delay"].get_value<float>();
|
||||
config.interval_spawn = yaml["spawn_interval"].get_value<float>();
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool {
|
||||
try {
|
||||
if (!yaml.contains("pentagon") || !yaml.contains("cuadrado") ||
|
||||
!yaml.contains("molinillo")) {
|
||||
std::cerr << "[StageLoader] Error: enemy_distribution incompleta" << '\n';
|
||||
auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool {
|
||||
try {
|
||||
if (!yaml.contains("pentagon") || !yaml.contains("cuadrado") ||
|
||||
!yaml.contains("molinillo")) {
|
||||
std::cerr << "[StageLoader] Error: enemy_distribution incompleta" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
dist.pentagon = yaml["pentagon"].get_value<uint8_t>();
|
||||
dist.cuadrado = yaml["cuadrado"].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
|
||||
int sum = dist.pentagon + dist.cuadrado + dist.molinillo + dist.star;
|
||||
if (sum != 100) {
|
||||
std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
dist.pentagon = yaml["pentagon"].get_value<uint8_t>();
|
||||
dist.cuadrado = yaml["cuadrado"].get_value<uint8_t>();
|
||||
dist.molinillo = yaml["molinillo"].get_value<uint8_t>();
|
||||
|
||||
// Validar que suma 100
|
||||
int sum = dist.pentagon + dist.cuadrado + dist.molinillo;
|
||||
if (sum != 100) {
|
||||
std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool {
|
||||
try {
|
||||
if (!yaml.contains("speed_multiplier") || !yaml.contains("rotation_multiplier") ||
|
||||
!yaml.contains("tracking_strength")) {
|
||||
std::cerr << "[StageLoader] Error: difficulty_multipliers incompleta" << '\n';
|
||||
auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool {
|
||||
try {
|
||||
if (!yaml.contains("speed_multiplier") || !yaml.contains("rotation_multiplier") ||
|
||||
!yaml.contains("tracking_strength")) {
|
||||
std::cerr << "[StageLoader] Error: difficulty_multipliers incompleta" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
mult.velocity = yaml["speed_multiplier"].get_value<float>();
|
||||
mult.rotation = yaml["rotation_multiplier"].get_value<float>();
|
||||
mult.tracking_strength = yaml["tracking_strength"].get_value<float>();
|
||||
|
||||
// Validar rangs raonables
|
||||
if (mult.velocity < 0.1F || mult.velocity > 5.0F) {
|
||||
std::cerr << "[StageLoader] Warning: speed_multiplier fuera de rang (0.1-5.0)" << '\n';
|
||||
}
|
||||
if (mult.rotation < 0.1F || mult.rotation > 5.0F) {
|
||||
std::cerr << "[StageLoader] Warning: rotation_multiplier fuera de rang (0.1-5.0)" << '\n';
|
||||
}
|
||||
if (mult.tracking_strength < 0.0F || mult.tracking_strength > 2.0F) {
|
||||
std::cerr << "[StageLoader] Warning: tracking_strength fuera de rang (0.0-2.0)" << '\n';
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Error parsing multipliers: " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
mult.velocity = yaml["speed_multiplier"].get_value<float>();
|
||||
mult.rotation = yaml["rotation_multiplier"].get_value<float>();
|
||||
mult.tracking_strength = yaml["tracking_strength"].get_value<float>();
|
||||
|
||||
// Validar rangs raonables
|
||||
if (mult.velocity < 0.1F || mult.velocity > 5.0F) {
|
||||
std::cerr << "[StageLoader] Warning: speed_multiplier fuera de rang (0.1-5.0)" << '\n';
|
||||
}
|
||||
if (mult.rotation < 0.1F || mult.rotation > 5.0F) {
|
||||
std::cerr << "[StageLoader] Warning: rotation_multiplier fuera de rang (0.1-5.0)" << '\n';
|
||||
}
|
||||
if (mult.tracking_strength < 0.0F || mult.tracking_strength > 2.0F) {
|
||||
std::cerr << "[StageLoader] Warning: tracking_strength fuera de rang (0.0-2.0)" << '\n';
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[StageLoader] Error parsing multipliers: " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn {
|
||||
if (mode_str == "progressive") {
|
||||
auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn {
|
||||
if (mode_str == "progressive") {
|
||||
return ModeSpawn::PROGRESSIVE;
|
||||
}
|
||||
if (mode_str == "immediate") {
|
||||
return ModeSpawn::IMMEDIATE;
|
||||
}
|
||||
if (mode_str == "wave") {
|
||||
return ModeSpawn::WAVE;
|
||||
}
|
||||
std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str
|
||||
<< "', usant PROGRESSIVE" << '\n';
|
||||
return ModeSpawn::PROGRESSIVE;
|
||||
}
|
||||
if (mode_str == "immediate") {
|
||||
return ModeSpawn::IMMEDIATE;
|
||||
}
|
||||
if (mode_str == "wave") {
|
||||
return ModeSpawn::WAVE;
|
||||
}
|
||||
std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str
|
||||
<< "', usant PROGRESSIVE" << '\n';
|
||||
return ModeSpawn::PROGRESSIVE;
|
||||
}
|
||||
|
||||
auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool {
|
||||
if (config.stages.empty()) {
|
||||
std::cerr << "[StageLoader] Error: sin stage carregat" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.stages.size() != config.metadata.total_stages) {
|
||||
std::cerr << "[StageLoader] Warning: nombre de stages (" << config.stages.size()
|
||||
<< ") no coincideix con metadata.total_stages ("
|
||||
<< static_cast<int>(config.metadata.total_stages) << ")" << '\n';
|
||||
}
|
||||
|
||||
// Validar stage_id consecutius
|
||||
for (size_t i = 0; i < config.stages.size(); i++) {
|
||||
if (config.stages[i].stage_id != i + 1) {
|
||||
std::cerr << "[StageLoader] Error: stage_id no consecutius (esperat "
|
||||
<< i + 1 << ", trobat " << static_cast<int>(config.stages[i].stage_id)
|
||||
<< ")" << '\n';
|
||||
auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool {
|
||||
if (config.stages.empty()) {
|
||||
std::cerr << "[StageLoader] Error: sin stage carregat" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.stages.size() != config.metadata.total_stages) {
|
||||
std::cerr << "[StageLoader] Warning: nombre de stages (" << config.stages.size()
|
||||
<< ") no coincideix con metadata.total_stages ("
|
||||
<< static_cast<int>(config.metadata.total_stages) << ")" << '\n';
|
||||
}
|
||||
|
||||
// Validar stage_id consecutius
|
||||
for (size_t i = 0; i < config.stages.size(); i++) {
|
||||
if (config.stages[i].stage_id != i + 1) {
|
||||
std::cerr << "[StageLoader] Error: stage_id no consecutius (esperat "
|
||||
<< i + 1 << ", trobat " << static_cast<int>(config.stages[i].stage_id)
|
||||
<< ")" << '\n';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace StageSystem
|
||||
|
||||
@@ -8,86 +8,12 @@
|
||||
#include "core/physics/collision.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/constants.hpp"
|
||||
#include "game/entities/bullet_config.hpp"
|
||||
#include "game/systems/enemy_event_dispatcher.hpp"
|
||||
|
||||
namespace Systems::Collision {
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t NO_SHOOTER = 0xFF;
|
||||
|
||||
// Lookup tabla puntos / color por tipo de enemy (mantiene la lógica
|
||||
// anterior pero centralizada para reutilizar entre paths).
|
||||
auto scoreForType(EnemyType type) -> int {
|
||||
switch (type) {
|
||||
case EnemyType::PENTAGON:
|
||||
return Defaults::Enemies::Scoring::PENTAGON_SCORE;
|
||||
case EnemyType::SQUARE:
|
||||
return Defaults::Enemies::Scoring::SQUARE_SCORE;
|
||||
case EnemyType::PINWHEEL:
|
||||
return Defaults::Enemies::Scoring::PINWHEEL_SCORE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto colorForType(EnemyType type) -> SDL_Color {
|
||||
switch (type) {
|
||||
case EnemyType::PENTAGON:
|
||||
return Defaults::Palette::PENTAGON;
|
||||
case EnemyType::SQUARE:
|
||||
return Defaults::Palette::SQUARE;
|
||||
case EnemyType::PINWHEEL:
|
||||
return Defaults::Palette::PINWHEEL;
|
||||
}
|
||||
return SDL_Color{};
|
||||
}
|
||||
|
||||
// Mata al enemy con explosión: floating score, debris con velocity heredada,
|
||||
// sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador.
|
||||
// CRUCIAL: leer velocity/datos ANTES de destruir() (que zera la velocity).
|
||||
void explodeNow(Context& ctx, Enemy& enemy, uint8_t shooter_id) {
|
||||
const Vec2 ENEMY_POS = enemy.getCenter();
|
||||
const Vec2 ENEMY_VEL = enemy.getVelocityVector();
|
||||
const float BRIGHTNESS = enemy.getBrightness();
|
||||
const auto SHAPE = enemy.getShape();
|
||||
const EnemyType TYPE = enemy.getType();
|
||||
|
||||
const int POINTS = scoreForType(TYPE);
|
||||
const SDL_Color COLOR = colorForType(TYPE);
|
||||
|
||||
if (shooter_id != NO_SHOOTER) {
|
||||
ctx.score_per_player[shooter_id] += POINTS;
|
||||
}
|
||||
ctx.floating_score_manager.crear(POINTS, ENEMY_POS);
|
||||
|
||||
enemy.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,
|
||||
Defaults::Palette::WOUNDED);
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -105,7 +31,7 @@ namespace Systems::Collision {
|
||||
0.0F, // sense velocity angular heretada
|
||||
0.0F, // sense rotació visual heretada
|
||||
Defaults::Sound::HIT,
|
||||
Defaults::Palette::BULLET,
|
||||
bullet.getConfig().colors.normal,
|
||||
Defaults::Physics::Debris::TEMPS_VIDA,
|
||||
Defaults::Physics::Debris::ACCELERACIO,
|
||||
1); // sense duplicat de segments
|
||||
@@ -121,31 +47,16 @@ namespace Systems::Collision {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// *** COLISIÓN bullet → enemy ***
|
||||
// Empuje físico cuasi-realista: el impulse és el moment de la bala
|
||||
// (m·v) multiplicat pel factor de transferència. Direcció = vector
|
||||
// velocity de la bala (cap a on viatjava).
|
||||
const Vec2 IMPULSE = bullet.getBody().velocity *
|
||||
(bullet.getBody().mass * Defaults::Physics::Bullet::IMPACT_MOMENTUM_FACTOR);
|
||||
enemy.applyImpulse(IMPULSE);
|
||||
|
||||
const uint8_t SHOOTER = bullet.getOwnerId();
|
||||
|
||||
if (enemy.isWounded()) {
|
||||
// Segundo impacto sobre enemy ya herido → muerte instantánea,
|
||||
// puntos al nuevo shooter.
|
||||
explodeNow(ctx, enemy, SHOOTER);
|
||||
} else {
|
||||
// Primer impacto → entra en estado herido (explosión diferida).
|
||||
enemy.hurt(SHOOTER);
|
||||
}
|
||||
|
||||
// La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu
|
||||
// al YAML de l'enemic via la secció `events:`. Aquí només dispatchem.
|
||||
Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, bullet.getOwnerId(), &bullet);
|
||||
breakBullet(ctx.debris_manager, bullet);
|
||||
break; // Una bala impacta a un enemy y muere
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +67,7 @@ namespace Systems::Collision {
|
||||
continue;
|
||||
}
|
||||
enemy.consumeWoundExpired();
|
||||
explodeNow(ctx, enemy, enemy.getLastHitBy());
|
||||
Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HURT_END, enemy.getLastHitBy());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +135,8 @@ namespace Systems::Collision {
|
||||
// Segon impacte durant HURT → mort. Aplica un impuls afegit
|
||||
// perquè l'enemic surti disparat (feedback visible).
|
||||
const Vec2 SHIP_VEL = ctx.ships[i].getVelocityVector();
|
||||
const Vec2 IMPULSE = SHIP_VEL * (Defaults::Ship::MASS * Defaults::Physics::Ship::DEATH_IMPACT_MOMENTUM_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);
|
||||
touched_enemy->applyImpulse(IMPULSE);
|
||||
ctx.on_player_hit(i);
|
||||
} else {
|
||||
@@ -268,7 +180,7 @@ namespace Systems::Collision {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -277,7 +189,7 @@ namespace Systems::Collision {
|
||||
// de la bala a la nau ABANS de on_player_hit perquè tocado()
|
||||
// captura la velocitat per als debris (si no, queden quiets).
|
||||
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.on_player_hit(player_id);
|
||||
ctx.lives_per_player[BULLET_OWNER]++;
|
||||
@@ -304,12 +216,12 @@ namespace Systems::Collision {
|
||||
float min_y;
|
||||
float max_y;
|
||||
Constants::getPlayAreaBounds(min_x, max_x, min_y, max_y);
|
||||
constexpr float R = Defaults::Entities::BULLET_RADIUS;
|
||||
|
||||
for (auto& bullet : bullets) {
|
||||
if (!bullet.isActive()) {
|
||||
continue;
|
||||
}
|
||||
const float R = bullet.getCollisionRadius();
|
||||
const Vec2& pos = bullet.getCenter();
|
||||
if (pos.x < min_x + R || pos.x > max_x - 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
|
||||
@@ -1,92 +1,99 @@
|
||||
// pack_resources.cpp - Utilitat per crear paquets de recursos
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "../../source/core/resources/resource_pack.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
void print_usage(const char* program_name) {
|
||||
std::cout << "Ús: " << program_name << " [opcions] [directori_entrada] [fitxer_sortida]\n";
|
||||
std::cout << "\nOpcions:\n";
|
||||
std::cout << " --list <fitxer> Llistar contingut d'un paquet\n";
|
||||
std::cout << "\nExemples:\n";
|
||||
std::cout << " " << program_name << " data resources.pack\n";
|
||||
std::cout << " " << program_name << " --list resources.pack\n";
|
||||
std::cout << "\nSi no s'especifiquen arguments, empaqueta 'data/' a 'resources.pack'\n";
|
||||
}
|
||||
#include "core/resources/resource_pack.hpp"
|
||||
#include "project.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void showHelp() {
|
||||
std::cout << Project::LONG_NAME << " - Resource Packer\n";
|
||||
std::cout << "==============================================\n";
|
||||
std::cout << "Usage: pack_resources [options] [input_dir] [output_file]\n\n";
|
||||
std::cout << "Options:\n";
|
||||
std::cout << " --help Show this help message\n";
|
||||
std::cout << " --list List contents of an existing pack file\n\n";
|
||||
std::cout << "Arguments:\n";
|
||||
std::cout << " input_dir Directory to pack (default: data)\n";
|
||||
std::cout << " output_file Pack file name (default: resources.pack)\n";
|
||||
}
|
||||
|
||||
void listPackContents(const std::string& pack_file) {
|
||||
Resource::Pack pack;
|
||||
if (!pack.loadPack(pack_file)) {
|
||||
std::cerr << "Error: cannot open pack file: " << pack_file << '\n';
|
||||
return;
|
||||
}
|
||||
auto resources = pack.getResourceList();
|
||||
std::cout << "Pack file: " << pack_file << '\n';
|
||||
std::cout << "Resources: " << resources.size() << '\n';
|
||||
for (const auto& r : resources) { std::cout << " " << r << '\n'; }
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::string input_dir = "data";
|
||||
std::string data_dir = "data";
|
||||
std::string output_file = "resources.pack";
|
||||
bool list_mode = false;
|
||||
bool data_dir_set = false;
|
||||
|
||||
// Processar arguments
|
||||
if (argc == 2 && std::string(argv[1]) == "--help") {
|
||||
print_usage(argv[0]);
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
if (arg == "--help" || arg == "-h") {
|
||||
showHelp();
|
||||
return 0;
|
||||
}
|
||||
if (arg == "--list") {
|
||||
list_mode = true;
|
||||
if (i + 1 < argc) { output_file = argv[++i]; }
|
||||
continue;
|
||||
}
|
||||
if (!arg.empty() && arg[0] != '-') {
|
||||
if (!data_dir_set) {
|
||||
data_dir = arg;
|
||||
data_dir_set = true;
|
||||
} else {
|
||||
output_file = arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (list_mode) {
|
||||
listPackContents(output_file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Mode --list
|
||||
if (argc == 3 && std::string(argv[1]) == "--list") {
|
||||
Resource::Pack pack;
|
||||
if (!pack.loadPack(argv[2])) {
|
||||
std::cerr << "ERROR: No es pot carregar " << argv[2] << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << Project::LONG_NAME << " - Resource Packer\n";
|
||||
std::cout << "==============================================\n";
|
||||
std::cout << "Input directory: " << data_dir << '\n';
|
||||
std::cout << "Output file: " << output_file << '\n';
|
||||
|
||||
std::cout << "Contingut de " << argv[2] << ":\n";
|
||||
auto resources = pack.getResourceList();
|
||||
std::cout << "Total: " << resources.size() << " recursos\n\n";
|
||||
|
||||
for (const auto& name : resources) {
|
||||
std::cout << " " << name << "\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Mode empaquetar
|
||||
if (argc >= 3) {
|
||||
input_dir = argv[1];
|
||||
output_file = argv[2];
|
||||
}
|
||||
|
||||
// Verificar que existeix el directori
|
||||
if (!std::filesystem::exists(input_dir)) {
|
||||
std::cerr << "ERROR: Directori no trobat: " << input_dir << "\n";
|
||||
if (!std::filesystem::exists(data_dir)) {
|
||||
std::cerr << "Error: input directory does not exist: " << data_dir << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!std::filesystem::is_directory(input_dir)) {
|
||||
std::cerr << "ERROR: " << input_dir << " no és un directori\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Crear paquet
|
||||
std::cout << "Creant paquet de recursos...\n";
|
||||
std::cout << " Entrada: " << input_dir << "\n";
|
||||
std::cout << " Sortida: " << output_file << "\n\n";
|
||||
|
||||
Resource::Pack pack;
|
||||
if (!pack.addDirectory(input_dir)) {
|
||||
std::cerr << "ERROR: No s'ha pogut afegir el directori\n";
|
||||
std::cout << "Scanning and packing resources...\n";
|
||||
if (!pack.addDirectory(data_dir)) {
|
||||
std::cerr << "Error: failed to add directory to pack\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "Found " << pack.getResourceList().size() << " resources\n";
|
||||
|
||||
std::cout << "Saving pack file...\n";
|
||||
if (!pack.savePack(output_file)) {
|
||||
std::cerr << "ERROR: No s'ha pogut guardar el paquet\n";
|
||||
std::cerr << "Error: failed to save pack file\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Resum
|
||||
auto resources = pack.getResourceList();
|
||||
std::cout << "\n";
|
||||
std::cout << "✓ Paquet creat amb èxit!\n";
|
||||
std::cout << " Recursos: " << resources.size() << "\n";
|
||||
|
||||
// Mostrar mida del fitxer
|
||||
auto file_size = std::filesystem::file_size(output_file);
|
||||
std::cout << " Mida: " << (file_size / 1024) << " KB\n";
|
||||
|
||||
auto file_size = std::filesystem::file_size(std::filesystem::path(output_file));
|
||||
std::cout << "Pack file created: " << output_file << " ("
|
||||
<< (static_cast<double>(file_size) / 1024.0 / 1024.0) << " MB)\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user