diff --git a/data/entities/big_pentagon/big_pentagon.yaml b/data/entities/big_pentagon/big_pentagon.yaml new file mode 100644 index 0000000..5314dec --- /dev/null +++ b/data/entities/big_pentagon/big_pentagon.yaml @@ -0,0 +1,82 @@ +name: big_pentagon +ai_type: big_pentagon # Validat contra el directori; mapeja a EnemyType::BIG_PENTAGON. + +# Shape circular pròpia (anell exterior + anell interior + 6 radis + nucli), +# pensada per llegir-se com a "reactor / orb" amb més detall que els enemics +# petits. +shape: + path: enemy_big_orb.shp + scale: 1.5 + collision_factor: 1.0 + +physics: + mass: 50.0 # Molt pesat: una bala el frena un poc però no el "envia a passejar". + speed: 50.0 # Avança decidit cap al ship (no és lent passiu, és amenaça constant). + rotation_delta_min: 0.3 + rotation_delta_max: 1.5 + restitution: 1.0 + linear_damping: 0.0 + angular_damping: 0.0 + +ai: + # Persecució contínua del ship més proper. chase_strength alt (1.0 = ~1s + # per realinear-se) perquè, encara que una bala l'empentja lateralment, + # ràpidament torna a posar la seua proa cap al jugador. + movement: + type: chase + chase_strength: 1.0 + +animation: + pulse: + trigger_prob_per_second: 0.01 + duration_min: 1.0 + duration_max: 3.0 + amplitude_min: 0.08 + amplitude_max: 0.20 + frequency_min: 1.5 + frequency_max: 3.0 + rotation_accel: + trigger_prob_per_second: 0.02 + duration_min: 3.0 + duration_max: 8.0 + multiplier_min: 0.3 + multiplier_max: 4.0 + +wounded: + duration: 1.5 # Una mica més llarg que els altres (és un boss). + blink_hz: 10.0 + +spawn: + invulnerability_duration: 3.0 + invulnerability_brightness_start: 0.3 + invulnerability_brightness_end: 0.7 + invulnerability_scale_start: 0.0 + invulnerability_scale_end: 1.0 + safety_distance: 54.0 # 1.5× del normal (alineat amb scale 1.5). + +colors: + normal: [66, 195, 208] # #42C3D0 — turquesa-cyan distintiu per al boss. + wounded: [255, 220, 60] + +score: 500 # 5x un enemic normal: aguanta 10x més. + +# Estrenant el sistema HP: 10 unitats. Cada bala fa decrease_health + flash +# + create_debris_partial (xip a 0.3x) + create_fireworks_small (espurna). +# Al 10è hit (HP=0), on_no_health encadena destroy directe — sense passar +# per wounded (com Star). 10 HP ja és prou dificultat sense afegir un hit +# extra. +health: 10 + +events: + on_hit: + - action: decrease_health # primer: si arriba a 0 dispara on_no_health + #- action: flash # feedback visual de damage parcial + - action: create_debris_partial # xip a 0.3x mida (sense ser letal) + - action: create_fireworks_small # espurna a cada hit (12 punts, lent) + - action: apply_impulse # empenta el cos (skip si will_die) + on_no_health: + - action: destroy # mort directa, sense wounded + on_destroy: + - action: add_score + - action: create_debris # explosió completa + - action: create_fireworks diff --git a/data/entities/pentagon/pentagon.yaml b/data/entities/pentagon/pentagon.yaml index 55eb8cf..baf4096 100644 --- a/data/entities/pentagon/pentagon.yaml +++ b/data/entities/pentagon/pentagon.yaml @@ -55,10 +55,14 @@ colors: score: 100 events: - # Comportament clàssic: dos impactes per matar (set_hurt entra wounded; - # el segon hit detecta wounded i destrueix automàticament). + # HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort. + # decrease_health primer perquè si la mort cau aquí (segon hit durant wounded), + # el dispatcher salta la resta del chain (incloent apply_impulse) sobre el + # cos ja destruït. on_hit: + - action: decrease_health - action: apply_impulse + on_no_health: - action: set_hurt on_hurt_end: - action: destroy diff --git a/data/entities/pinwheel/pinwheel.yaml b/data/entities/pinwheel/pinwheel.yaml index 6a9f465..bf782bd 100644 --- a/data/entities/pinwheel/pinwheel.yaml +++ b/data/entities/pinwheel/pinwheel.yaml @@ -55,8 +55,11 @@ colors: score: 200 events: + # HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort. on_hit: + - action: decrease_health - action: apply_impulse + on_no_health: - action: set_hurt on_hurt_end: - action: destroy diff --git a/data/entities/square/square.yaml b/data/entities/square/square.yaml index 703bf3d..8de6d1c 100644 --- a/data/entities/square/square.yaml +++ b/data/entities/square/square.yaml @@ -56,8 +56,11 @@ colors: score: 150 events: + # HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort. on_hit: + - action: decrease_health - action: apply_impulse + on_no_health: - action: set_hurt on_hurt_end: - action: destroy diff --git a/data/entities/star/star.yaml b/data/entities/star/star.yaml index 4ed165e..8ba95f8 100644 --- a/data/entities/star/star.yaml +++ b/data/entities/star/star.yaml @@ -66,8 +66,10 @@ score: 100 events: # STAR: mor al primer impacte, sense passar per wounded. + # HP=1 (default): decrement → on_no_health → destroy directe (sense wounded). on_hit: - - action: apply_impulse + - action: decrease_health + on_no_health: - action: destroy on_destroy: - action: add_score diff --git a/data/shapes/enemy_big_orb.shp b/data/shapes/enemy_big_orb.shp new file mode 100644 index 0000000..39c3c87 --- /dev/null +++ b/data/shapes/enemy_big_orb.shp @@ -0,0 +1,32 @@ +# enemy_big_orb.shp - ORNI enemic gegant (orb circular, doble anell amb radis) +# © 2026 JailDesigner +# +# Forma "reactor / boss circular" — més detall que els enemics petits perquè +# es renderitza a escala 1.5x i ha de llegir-se com a amenaça gran. +# - Anell exterior: dodecàgon (12 vèrtexs) — aparença circular suau, radi 20. +# - Anell interior: hexàgon (6 vèrtexs, rotat 30°) — radi 10. +# - 6 radis curts que connecten l'anell interior amb l'exterior. +# - Petit "+" central com a nucli. +# Bounding radius natiu = 20 (alineat amb la resta d'enemics). + +name: enemy_big_orb +scale: 1.0 +center: 0, 0 + +# Anell exterior (dodecàgon, vèrtex apuntant amunt) +polyline: 0,-20 10,-17.32 17.32,-10 20,0 17.32,10 10,17.32 0,20 -10,17.32 -17.32,10 -20,0 -17.32,-10 -10,-17.32 0,-20 + +# Anell interior (hexàgon, vèrtex apuntant a la dreta — rotat 30° respecte l'exterior) +polyline: 5,-8.66 10,0 5,8.66 -5,8.66 -10,0 -5,-8.66 5,-8.66 + +# 6 radis: del vèrtex de l'hexàgon interior al vèrtex corresponent del dodecàgon exterior +line: 5,-8.66 10,-17.32 +line: 10,0 20,0 +line: 5,8.66 10,17.32 +line: -5,8.66 -10,17.32 +line: -10,0 -20,0 +line: -5,-8.66 -10,-17.32 + +# Nucli central: petit "+" (2 segments creuats, radi 3) +line: -3,0 3,0 +line: 0,-3 0,3 diff --git a/data/stages/stages.yaml b/data/stages/stages.yaml index 03824a5..9a9b885 100644 --- a/data/stages/stages.yaml +++ b/data/stages/stages.yaml @@ -7,7 +7,7 @@ metadata: description: "Progressive difficulty curve from novice to expert" stages: - # STAGE 1: Tutorial - Mix de tots 4 tipus al 25% per mostrar-los junts + # STAGE 1: Tutorial - 4 tipus (sense star: les bales fan injugable el test). - stage_id: 1 total_enemies: 50 spawn_config: @@ -15,10 +15,10 @@ stages: initial_delay: 0.3 spawn_interval: 0.4 enemy_distribution: - pentagon: 25 + pentagon: 30 cuadrado: 25 molinillo: 25 - star: 25 + big_pentagon: 20 difficulty_multipliers: speed_multiplier: 0.7 rotation_multiplier: 0.8 diff --git a/source/core/defaults/enemies.hpp b/source/core/defaults/enemies.hpp index c838f26..5ffe89d 100644 --- a/source/core/defaults/enemies.hpp +++ b/source/core/defaults/enemies.hpp @@ -16,3 +16,30 @@ namespace Defaults::Enemies::Spawn { constexpr int MAX_SPAWN_ATTEMPTS = 50; } // namespace Defaults::Enemies::Spawn + +namespace Defaults::Enemies::Visual { + + // Duració del "flash" que dispara l'acció FLASH (feedback per impacte + // parcial en enemics HP>1). Curt: l'efecte ha de llegir-se com un cop, + // no com una transició. + constexpr float FLASH_DURATION = 0.08F; + +} // namespace Defaults::Enemies::Visual + +namespace Defaults::Enemies::Debris { + + // Escala dels fragments per a l'acció CREATE_DEBRIS_PARTIAL (xip d'impacte + // en enemics HP>1). 0.3 = trossos petits, com de "casc esquerdat". + constexpr float PARTIAL_PIECE_SCALE = 0.3F; + +} // namespace Defaults::Enemies::Debris + +namespace Defaults::Enemies::Fireworks { + + // Paràmetres del firework "petit" per a l'acció CREATE_FIREWORKS_SMALL + // (feedback per impacte parcial en enemics HP>1). Pocs punts i baixa + // velocitat: una espurna breu, no una explosió. + constexpr int SMALL_N_POINTS = 20; + constexpr float SMALL_SPEED = 250.0F; + +} // namespace Defaults::Enemies::Fireworks diff --git a/source/game/effects/debris_manager.cpp b/source/game/effects/debris_manager.cpp index 946762e..4cdb8e3 100644 --- a/source/game/effects/debris_manager.cpp +++ b/source/game/effects/debris_manager.cpp @@ -58,7 +58,8 @@ namespace Effects { float lifetime, float friction, int segment_multiplier, - const Vec2& bullet_impulse_velocity) { + const Vec2& bullet_impulse_velocity, + float piece_scale) { if (!shape || !shape->isValid()) { return; } @@ -85,7 +86,7 @@ namespace Effects { Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale); // Si el pool es ple, no té sentit continuar amb la resta de segments - if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction, bullet_impulse_velocity)) { + if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction, bullet_impulse_velocity, piece_scale)) { return; } } @@ -111,26 +112,36 @@ namespace Effects { return segments; } - auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity) -> bool { + auto DebrisManager::spawnDebris(const Vec2& world_p1_in, const Vec2& world_p2_in, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool { Debris* debris = findFreeSlot(); if (debris == nullptr) { std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n"; return false; } + // Escala el segment al voltant del seu punt mitjà segons piece_scale + // (1.0 = original; 0.3 = "esquerda petita"). La resta del càlcul (angle, + // half_length, p1/p2) en deriva naturalment. + const Vec2 MID = {.x = (world_p1_in.x + world_p2_in.x) / 2.0F, + .y = (world_p1_in.y + world_p2_in.y) / 2.0F}; + const Vec2 WORLD_P1 = {.x = MID.x + ((world_p1_in.x - MID.x) * piece_scale), + .y = MID.y + ((world_p1_in.y - MID.y) * piece_scale)}; + const Vec2 WORLD_P2 = {.x = MID.x + ((world_p2_in.x - MID.x) * piece_scale), + .y = MID.y + ((world_p2_in.y - MID.y) * piece_scale)}; + // Geometria autoritaritzada: centro + original_angle + original_half_length. // p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades. - const float DX = world_p2.x - world_p1.x; - const float DY = world_p2.y - world_p1.y; - debris->centro = {.x = (world_p1.x + world_p2.x) / 2.0F, - .y = (world_p1.y + world_p2.y) / 2.0F}; + const float DX = WORLD_P2.x - WORLD_P1.x; + const float DY = WORLD_P2.y - WORLD_P1.y; + debris->centro = {.x = (WORLD_P1.x + WORLD_P2.x) / 2.0F, + .y = (WORLD_P1.y + WORLD_P2.y) / 2.0F}; debris->original_angle = std::atan2(DY, DX); debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F; - debris->p1 = world_p1; - debris->p2 = world_p2; + debris->p1 = WORLD_P1; + debris->p2 = WORLD_P2; // Direcció radial (desde el centro hacia el segment) - Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro); + Vec2 direccio = computeExplosionDirection(WORLD_P1, WORLD_P2, centro); // Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte + // velocitat de la bala escalada per BULLET_IMPULSE_FACTOR). diff --git a/source/game/effects/debris_manager.hpp b/source/game/effects/debris_manager.hpp index c58bc2f..4a64be1 100644 --- a/source/game/effects/debris_manager.hpp +++ b/source/game/effects/debris_manager.hpp @@ -52,6 +52,9 @@ namespace Effects { // Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR, independent de // velocitat_objecte. Permet que els trossos "salten amb la força de la bala" // encara que el cos sigui pesat i amb prou feines es mogui. + // - piece_scale: multiplicador de la longitud de cada fragment al spawn + // (per defecte 1.0). Útil per a debris "parcial" d'impactes no letals + // en enemics HP>1 (trossos petits, com d'esquerda). void explode(const std::shared_ptr& shape, const Vec2& centro, float angle, @@ -66,7 +69,8 @@ namespace Effects { float lifetime = Defaults::Physics::Debris::TEMPS_VIDA, float friction = Defaults::Physics::Debris::ACCELERACIO, int segment_multiplier = 1, - const Vec2& bullet_impulse_velocity = {.x = 0.0F, .y = 0.0F}); + const Vec2& bullet_impulse_velocity = {.x = 0.0F, .y = 0.0F}, + float piece_scale = 1.0F); // Actualitzar todos los fragments active void update(float delta_time); @@ -103,7 +107,7 @@ namespace Effects { -> std::vector>; // Inicialitza un debris en un slot lliure i el deixa actiu. Retorna // false si el pool está ple (la cridadora ha d'aturar el bucle). - auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity) -> bool; + auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool; static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular); static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual); }; diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index b45d782..b9ee103 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -133,6 +133,9 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { invulnerability_timer_ = cfg.spawn.invulnerability_duration; brightness_ = cfg.spawn.invulnerability_brightness_start; + health_ = cfg.health; + flash_timer_ = 0.0F; + is_active_ = true; } @@ -150,6 +153,11 @@ void Enemy::update(float delta_time) { } } + if (flash_timer_ > 0.0F) { + flash_timer_ -= delta_time; + flash_timer_ = std::max(flash_timer_, 0.0F); + } + if (invulnerability_timer_ > 0.0F) { invulnerability_timer_ -= delta_time; invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F); @@ -192,7 +200,15 @@ void Enemy::draw() const { } } - Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, brightness_, color); + // Flash d'impacte parcial (HP>1): força el color a blanc i el brillo a + // 1.0 durant la finestra de flash. Té prioritat sobre el blink wounded. + float effective_brightness = brightness_; + if (flash_timer_ > 0.0F) { + color = SDL_Color{.r = 255, .g = 255, .b = 255, .a = 255}; + effective_brightness = 1.0F; + } + + Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, effective_brightness, color); } void Enemy::destroy() { @@ -203,6 +219,7 @@ void Enemy::destroy() { wounded_timer_ = 0.0F; wound_expired_this_frame_ = false; last_hit_by_ = 0xFF; + flash_timer_ = 0.0F; } void Enemy::hurt(uint8_t shooter_id) { diff --git a/source/game/entities/enemy.hpp b/source/game/entities/enemy.hpp index 3250925..aab39fe 100644 --- a/source/game/entities/enemy.hpp +++ b/source/game/entities/enemy.hpp @@ -8,6 +8,7 @@ #include #include +#include "core/defaults/enemies.hpp" #include "core/entities/entity.hpp" #include "core/types.hpp" #include "game/entities/enemy_ai.hpp" @@ -16,10 +17,11 @@ class Ship; // Tipo de enemy enum class EnemyType : uint8_t { - PENTAGON = 0, // Pentágono esquivador (zigzag) - SQUARE = 1, // Square perseguidor (tracks ship) - PINWHEEL = 2, // Molinillo agresivo (rápido, girando) - STAR = 3 // Estrella de 5 puntes (clone visual de Pentagon, comportament zigzag) + PENTAGON = 0, // Pentágono esquivador (zigzag) + SQUARE = 1, // Square perseguidor (tracks ship) + PINWHEEL = 2, // Molinillo agresivo (rápido, girando) + STAR = 3, // Estrella de 5 puntes (clone visual de Pentagon, comportament zigzag) + BIG_PENTAGON = 4, // Pentàgon gegant tough (HP=10, chase lent — primer enemic HP>1) }; // Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp. @@ -116,6 +118,20 @@ class Enemy : public Entities::Entity { void consumeWoundExpired() { wound_expired_this_frame_ = false; } [[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; } + // Salut: decrementada per l'acció DECREASE_HEALTH al dispatcher d'events. + // Quan arriba a 0 o menys, el dispatcher dispara ON_NO_HEALTH (que + // típicament encadena SET_HURT o DESTROY al YAML). last_hit_by s'actualitza + // al decrement perquè la mort posterior atribueixi correctament el kill. + [[nodiscard]] auto getHealth() const -> int { return health_; } + void decrementHealth(uint8_t shooter_id = 0xFF) { + --health_; + last_hit_by_ = shooter_id; + } + + // Flash visual: brief impacto-feedback quan rep un hit no letal (HP>1). + // Disparat per l'acció FLASH; el render alça la lluminositat mentre dura. + void triggerFlash() { flash_timer_ = Defaults::Enemies::Visual::FLASH_DURATION; } + // Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico. void applyImpulse(const Vec2& impulse); @@ -154,6 +170,16 @@ class Enemy : public Entities::Entity { bool wound_expired_this_frame_{false}; uint8_t last_hit_by_{0xFF}; + // Salut per-instància. Reseteja a config_->health a init(); el dispatcher + // d'events la decrementa via DECREASE_HEALTH i dispara ON_NO_HEALTH quan + // creua zero. Permet enemics tough (HP>1) sense canvis al motor. + int health_{1}; + + // Flash visual temporitzat per a feedback d'impacte parcial (HP>1). + // L'acció FLASH el reseteja a FLASH_DURATION; draw() alça la lluminositat + // mentre dura, i update() el decrementa. + float flash_timer_{0.0F}; + // Métodos privados void updateAnimation(float delta_time); void updatePulse(float delta_time); diff --git a/source/game/entities/enemy_config.cpp b/source/game/entities/enemy_config.cpp index 0b91fee..19af094 100644 --- a/source/game/entities/enemy_config.cpp +++ b/source/game/entities/enemy_config.cpp @@ -30,6 +30,7 @@ namespace { if (s == "square") { return EnemyType::SQUARE; } if (s == "pinwheel") { return EnemyType::PINWHEEL; } if (s == "star") { return EnemyType::STAR; } + if (s == "big_pentagon") { return EnemyType::BIG_PENTAGON; } return std::nullopt; } @@ -177,13 +178,25 @@ namespace { return true; } + // health és opcional: si el YAML no l'inclou, el default {1} de l'struct + // ja cobreix el comportament de tots els enemics actuals (1 hit → mort). + void parseHealth(const fkyaml::node& node, int& out) { + if (node.contains("health")) { + out = node["health"].get_value(); + } + } + auto actionTypeFromString(const std::string& s) -> std::optional { 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_debris_partial") { return EnemyActionType::CREATE_DEBRIS_PARTIAL; } if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; } + if (s == "create_fireworks_small") { return EnemyActionType::CREATE_FIREWORKS_SMALL; } if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; } + if (s == "decrease_health") { return EnemyActionType::DECREASE_HEALTH; } + if (s == "flash") { return EnemyActionType::FLASH; } return std::nullopt; } @@ -340,6 +353,13 @@ namespace { out.movement.rotation_proximity_multiplier = legacy.rotation_proximity_multiplier; out.movement.proximity_distance = legacy.proximity_distance; break; + case EnemyType::BIG_PENTAGON: + // Sense legacy fallback: el YAML del big_pentagon ha de definir + // ai.movement explícitament. Default chase lent perquè el switch + // siga exhaustiu i no falli si algú omet el bloc ai. + out.movement.type = MovementType::CHASE; + out.movement.chase_strength = 0.3F; + break; } } @@ -371,6 +391,10 @@ namespace { if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) { return false; } + if (e.contains("on_no_health") && + !parseActionList(e["on_no_health"], name, "on_no_health", out.on_no_health)) { + return false; + } if (e.contains("on_hurt_end") && !parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) { return false; @@ -407,6 +431,7 @@ auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type) if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; } if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; } if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; } + parseHealth(node, cfg.health); if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; } if (!parseAi(node, cfg.name, cfg.ai_type, cfg.behavior, cfg.ai)) { return std::nullopt; } diff --git a/source/game/entities/enemy_config.hpp b/source/game/entities/enemy_config.hpp index 72ca35d..f85677e 100644 --- a/source/game/entities/enemy_config.hpp +++ b/source/game/entities/enemy_config.hpp @@ -100,6 +100,10 @@ struct EnemyConfig { SpawnCfg spawn; ColorsCfg colors; int score; + // Salut inicial: per defecte 1 (un balazo → on_no_health). Els YAMLs poden + // pujar-lo (p.ex. 10 per a un enemic tough). El sistema d'events és qui + // decideix què passa quan la salut arriba a 0 via on_no_health. + int health{1}; EnemyEventConfig events; EnemyAiConfig ai; diff --git a/source/game/entities/enemy_event.hpp b/source/game/entities/enemy_event.hpp index aa645fd..4c6e58b 100644 --- a/source/game/entities/enemy_event.hpp +++ b/source/game/entities/enemy_event.hpp @@ -11,18 +11,23 @@ #include 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) + ON_HIT, // Impactat per una bala + ON_NO_HEALTH, // health ha arribat a 0 o menys aquest frame (via DECREASE_HEALTH) + ON_HURT_END, // Timer wounded ha expirat aquest frame + ON_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 + 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_DEBRIS_PARTIAL, // Debris de xip parcial (trossos a escala 0.3, per hits HP>1) + CREATE_FIREWORKS, // Burst radial de firework + CREATE_FIREWORKS_SMALL, // Burst petit (pocs punts, poca velocitat) — feedback per hit + APPLY_IMPULSE, // Aplica l'impuls de la bala impactant + DECREASE_HEALTH, // Decrementa health_; si <=0, dispatcha ON_NO_HEALTH + FLASH, // Flash visual breu (feedback per impacte parcial) }; struct EnemyAction { @@ -31,6 +36,7 @@ struct EnemyAction { struct EnemyEventConfig { std::vector on_hit; + std::vector on_no_health; std::vector on_hurt_end; std::vector on_destroy; @@ -38,6 +44,8 @@ struct EnemyEventConfig { switch (event) { case EnemyEventType::ON_HIT: return on_hit; + case EnemyEventType::ON_NO_HEALTH: + return on_no_health; case EnemyEventType::ON_HURT_END: return on_hurt_end; case EnemyEventType::ON_DESTROY: diff --git a/source/game/entities/enemy_registry.cpp b/source/game/entities/enemy_registry.cpp index 6aa8087..79e1de3 100644 --- a/source/game/entities/enemy_registry.cpp +++ b/source/game/entities/enemy_registry.cpp @@ -13,6 +13,7 @@ EnemyConfig EnemyRegistry::pentagon_config; EnemyConfig EnemyRegistry::square_config; EnemyConfig EnemyRegistry::pinwheel_config; EnemyConfig EnemyRegistry::star_config; +EnemyConfig EnemyRegistry::big_pentagon_config; bool EnemyRegistry::loaded = false; namespace { @@ -38,10 +39,11 @@ 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); + loadOne("star", EnemyType::STAR, star_config) && + loadOne("big_pentagon", EnemyType::BIG_PENTAGON, big_pentagon_config); loaded = OK; if (OK) { - std::cout << "[EnemyRegistry] 4 configuracions d'enemic carregades.\n"; + std::cout << "[EnemyRegistry] 5 configuracions d'enemic carregades.\n"; } return OK; } @@ -60,6 +62,8 @@ auto EnemyRegistry::get(EnemyType type) -> const EnemyConfig& { return pinwheel_config; case EnemyType::STAR: return star_config; + case EnemyType::BIG_PENTAGON: + return big_pentagon_config; } std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n"; std::exit(EXIT_FAILURE); diff --git a/source/game/entities/enemy_registry.hpp b/source/game/entities/enemy_registry.hpp index 49e4d3c..466bcea 100644 --- a/source/game/entities/enemy_registry.hpp +++ b/source/game/entities/enemy_registry.hpp @@ -27,5 +27,6 @@ class EnemyRegistry { static EnemyConfig square_config; static EnemyConfig pinwheel_config; static EnemyConfig star_config; + static EnemyConfig big_pentagon_config; static bool loaded; }; diff --git a/source/game/stage_system/spawn_controller.cpp b/source/game/stage_system/spawn_controller.cpp index 2ec6314..c0e9c67 100644 --- a/source/game/stage_system/spawn_controller.cpp +++ b/source/game/stage_system/spawn_controller.cpp @@ -129,17 +129,21 @@ namespace StageSystem { // Weighted random selection based on distribution int rand_val = std::rand() % 100; + const auto& d = config_->distribucio; - if (std::cmp_less(rand_val, config_->distribucio.pentagon)) { + if (std::cmp_less(rand_val, d.pentagon)) { return EnemyType::PENTAGON; } - if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) { + if (rand_val < d.pentagon + d.cuadrado) { return EnemyType::SQUARE; } - if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado + config_->distribucio.molinillo) { + if (rand_val < d.pentagon + d.cuadrado + d.molinillo) { return EnemyType::PINWHEEL; } - return EnemyType::STAR; + if (rand_val < d.pentagon + d.cuadrado + d.molinillo + d.star) { + return EnemyType::STAR; + } + return EnemyType::BIG_PENTAGON; } void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) { diff --git a/source/game/stage_system/stage_config.hpp b/source/game/stage_system/stage_config.hpp index f990a98..0a05d2e 100644 --- a/source/game/stage_system/stage_config.hpp +++ b/source/game/stage_system/stage_config.hpp @@ -25,11 +25,12 @@ namespace StageSystem { // Distribució de type de enemigos (percentatges) struct DistribucioEnemics { - 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 + 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) + uint8_t big_pentagon{0}; // 0-100 (opcional; enemic gegant HP=10) + // Suma ha de ser 100, validat en StageLoader }; // Multiplicadors de dificultat @@ -60,7 +61,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 + distribucio.star == 100; + distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo + distribucio.star + distribucio.big_pentagon == 100; } }; diff --git a/source/game/stage_system/stage_loader.cpp b/source/game/stage_system/stage_loader.cpp index ae3ba5a..903cd21 100644 --- a/source/game/stage_system/stage_loader.cpp +++ b/source/game/stage_system/stage_loader.cpp @@ -171,11 +171,12 @@ namespace StageSystem { dist.pentagon = yaml["pentagon"].get_value(); dist.cuadrado = yaml["cuadrado"].get_value(); dist.molinillo = yaml["molinillo"].get_value(); - // 'star' és opcional per compatibilitat amb stages antics (default 0). + // 'star' i 'big_pentagon' són opcionals per compatibilitat amb stages antics (default 0). dist.star = yaml.contains("star") ? yaml["star"].get_value() : 0; + dist.big_pentagon = yaml.contains("big_pentagon") ? yaml["big_pentagon"].get_value() : 0; // Validar que suma 100 - int sum = dist.pentagon + dist.cuadrado + dist.molinillo + dist.star; + int sum = dist.pentagon + dist.cuadrado + dist.molinillo + dist.star + dist.big_pentagon; if (sum != 100) { std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n'; return false; diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index e7a32e5..fa67f14 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -83,36 +83,6 @@ namespace Systems::Collision { } } - void detectWoundedChain(Context& ctx) { - const std::size_t N = ctx.enemies.size(); - for (std::size_t i = 0; i < N; i++) { - Enemy& a = ctx.enemies[i]; - if (!a.isCollidable()) { - continue; - } - for (std::size_t j = i + 1; j < N; j++) { - Enemy& b = ctx.enemies[j]; - if (!b.isCollidable()) { - continue; - } - const bool A_WOUNDED = a.isWounded(); - const bool B_WOUNDED = b.isWounded(); - if (A_WOUNDED == B_WOUNDED) { - continue; // ambos sanos o ambos heridos: nada que propagar - } - if (!Physics::checkCollision(a, b, Defaults::Game::COLLISION_WOUNDED_CHAIN_AMPLIFIER)) { - continue; - } - // El sano queda herido, propagando el shooter original. - if (A_WOUNDED) { - b.hurt(a.getLastHitBy()); - } else { - a.hurt(b.getLastHitBy()); - } - } - } - } - void detectShipEnemy(Context& ctx) { constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; @@ -263,7 +233,9 @@ namespace Systems::Collision { void detectAll(Context& ctx) { processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame detectBulletEnemy(ctx); - detectWoundedChain(ctx); // un herit pot ferir a un sa al fregar-lo + // Wounded chain desactivat: era massa fàcil que un enemic ferit topés + // amb el big_pentagon (10 HP) i el matés instantàniament. La regla + // "ferit-toca-sa → ferit" queda permanentment fora. detectShipEnemy(ctx); detectBulletPlayer(ctx); detectEnemyBulletShip(ctx); diff --git a/source/game/systems/collision_system.hpp b/source/game/systems/collision_system.hpp index 9e28de6..46619ba 100644 --- a/source/game/systems/collision_system.hpp +++ b/source/game/systems/collision_system.hpp @@ -56,12 +56,6 @@ namespace Systems::Collision { // al `last_hit_by_` del enemy (si está set). void processWoundedDeaths(Context& ctx); - // Si un enemy herido colisiona con uno sano (ni herido ni invulnerable), - // el sano también queda herido (efecto cadena). Propaga `last_hit_by_` para - // que el shooter original siga acreditándose la muerte en cascada. El rebote - // físico ya lo resuelve PhysicsWorld; aquí solo propagamos el estado. - void detectWoundedChain(Context& ctx); - // Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id). void detectShipEnemy(Context& ctx); diff --git a/source/game/systems/enemy_event_dispatcher.cpp b/source/game/systems/enemy_event_dispatcher.cpp index e7c0f81..6e3bbe4 100644 --- a/source/game/systems/enemy_event_dispatcher.cpp +++ b/source/game/systems/enemy_event_dispatcher.cpp @@ -24,7 +24,10 @@ namespace Systems::EnemyEvents { ctx.floating_score_manager.crear(POINTS, enemy.getCenter()); } - void doCreateDebris(Systems::Collision::Context& ctx, const Enemy& enemy, const Bullet* bullet) { + // Helper compartit per CREATE_DEBRIS i CREATE_DEBRIS_PARTIAL: única + // crida a explode(), paràmetres alineats; només canvien piece_scale + // (1.0 explosió, 0.3 xip) i el color (cos vs hit-feedback). + void spawnDebrisForEnemy(Systems::Collision::Context& ctx, const Enemy& enemy, const Bullet* bullet, float piece_scale, SDL_Color color) { constexpr float SPEED_EXPLOSIO = 80.0F; const Vec2 INHERITED_VEL = enemy.getVelocityVector() * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE; @@ -40,21 +43,25 @@ namespace Systems::EnemyEvents { 0.0F, 0.0F, Defaults::Sound::EXPLOSION, - enemy.getConfig().colors.normal, + color, Defaults::Physics::Debris::ENEMY_LIFETIME, Defaults::Physics::Debris::ENEMY_FRICTION, Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER, - BULLET_VEL); + BULLET_VEL, + piece_scale); } - void doCreateFireworks(Systems::Collision::Context& ctx, const Enemy& enemy) { + // Helper compartit per CREATE_FIREWORKS i CREATE_FIREWORKS_SMALL: + // mateixa crida a spawn(); els callers decideixen line_color, glow_color, + // n_points i initial_speed segons el "tamany" del burst (mort vs hit). + void spawnFireworksForEnemy(Systems::Collision::Context& ctx, const Enemy& enemy, int n_points, float initial_speed, SDL_Color line_color, SDL_Color glow_color) { ctx.firework_manager.spawn(enemy.getCenter(), - Defaults::FX::Firework::DEFAULT_COLOR, - Defaults::FX::Firework::SPEED, - Defaults::FX::Firework::N_POINTS, + line_color, + initial_speed, + n_points, Defaults::FX::Firework::INITIAL_BRIGHTNESS, /*glow=*/true, - enemy.getConfig().colors.wounded); + glow_color); } void doApplyImpulse(Enemy& enemy, const Bullet* bullet) { @@ -88,6 +95,13 @@ namespace Systems::EnemyEvents { } for (const auto& action : actions) { + // Si una acció prèvia d'aquest chain ha destruït l'enemic + // (típicament DECREASE_HEALTH→ON_NO_HEALTH→SET_HURT-wounded→DESTROY), + // saltem la resta — no té sentit aplicar APPLY_IMPULSE o FLASH a un + // cos ja inactiu. + if (!enemy.isActive()) { + break; + } switch (action.type) { case EnemyActionType::SET_HURT: if (enemy.isWounded()) { @@ -106,16 +120,39 @@ namespace Systems::EnemyEvents { doAddScore(ctx, enemy, shooter_id); break; case EnemyActionType::CREATE_DEBRIS: - doCreateDebris(ctx, enemy, bullet); + // Explosió de mort: trossos en color cos (correcte físicament). + spawnDebrisForEnemy(ctx, enemy, bullet, 1.0F, enemy.getConfig().colors.normal); + break; + case EnemyActionType::CREATE_DEBRIS_PARTIAL: + // Xip d'impacte: trossos en color wounded (daurat) per + // diferenciar-los visualment del cos i marcar "damage". + spawnDebrisForEnemy(ctx, enemy, bullet, Defaults::Enemies::Debris::PARTIAL_PIECE_SCALE, enemy.getConfig().colors.wounded); break; case EnemyActionType::CREATE_FIREWORKS: - doCreateFireworks(ctx, enemy); + // Burst de mort: línia blanca + glow wounded (daurat) per + // marcar la mort com a esdeveniment "calent" i lluminós. + spawnFireworksForEnemy(ctx, enemy, Defaults::FX::Firework::N_POINTS, Defaults::FX::Firework::SPEED, Defaults::FX::Firework::DEFAULT_COLOR, enemy.getConfig().colors.wounded); + break; + case EnemyActionType::CREATE_FIREWORKS_SMALL: + // Espurna d'impacte: línia + glow tots dos en wounded + // (daurat) per contrastar amb el cos i unificar la "tema + // de damage" amb el debris parcial. + spawnFireworksForEnemy(ctx, enemy, Defaults::Enemies::Fireworks::SMALL_N_POINTS, Defaults::Enemies::Fireworks::SMALL_SPEED, enemy.getConfig().colors.wounded, enemy.getConfig().colors.wounded); break; case EnemyActionType::APPLY_IMPULSE: if (!will_die) { doApplyImpulse(enemy, bullet); } break; + case EnemyActionType::DECREASE_HEALTH: + enemy.decrementHealth(shooter_id); + if (enemy.getHealth() <= 0) { + dispatchEvent(ctx, enemy, EnemyEventType::ON_NO_HEALTH, shooter_id, bullet); + } + break; + case EnemyActionType::FLASH: + enemy.triggerFlash(); + break; } } }