From 9c0502eefbe74019ae273491129340533f36a9b0 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 25 May 2026 13:34:48 +0200 Subject: [PATCH] feat(enemy): sistema d'events declaratius via YAML --- data/entities/pentagon/pentagon.yaml | 13 +++ data/entities/pinwheel/pinwheel.yaml | 11 ++ data/entities/square/square.yaml | 11 ++ data/entities/star/star.yaml | 10 ++ source/game/entities/enemy_config.cpp | 74 +++++++++++++ source/game/entities/enemy_config.hpp | 2 + source/game/entities/enemy_event.hpp | 48 +++++++++ source/game/systems/collision_system.cpp | 77 ++----------- .../game/systems/enemy_event_dispatcher.cpp | 101 ++++++++++++++++++ .../game/systems/enemy_event_dispatcher.hpp | 23 ++++ 10 files changed, 299 insertions(+), 71 deletions(-) create mode 100644 source/game/entities/enemy_event.hpp create mode 100644 source/game/systems/enemy_event_dispatcher.cpp create mode 100644 source/game/systems/enemy_event_dispatcher.hpp diff --git a/data/entities/pentagon/pentagon.yaml b/data/entities/pentagon/pentagon.yaml index 0749834..55eb8cf 100644 --- a/data/entities/pentagon/pentagon.yaml +++ b/data/entities/pentagon/pentagon.yaml @@ -53,3 +53,16 @@ colors: 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 diff --git a/data/entities/pinwheel/pinwheel.yaml b/data/entities/pinwheel/pinwheel.yaml index 9b38b5f..6a9f465 100644 --- a/data/entities/pinwheel/pinwheel.yaml +++ b/data/entities/pinwheel/pinwheel.yaml @@ -53,3 +53,14 @@ colors: 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 diff --git a/data/entities/square/square.yaml b/data/entities/square/square.yaml index 23613ef..1d62445 100644 --- a/data/entities/square/square.yaml +++ b/data/entities/square/square.yaml @@ -53,3 +53,14 @@ colors: 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 diff --git a/data/entities/star/star.yaml b/data/entities/star/star.yaml index c870e69..00b6fb0 100644 --- a/data/entities/star/star.yaml +++ b/data/entities/star/star.yaml @@ -53,3 +53,13 @@ colors: 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 diff --git a/source/game/entities/enemy_config.cpp b/source/game/entities/enemy_config.cpp index 95d323e..4e56bcc 100644 --- a/source/game/entities/enemy_config.cpp +++ b/source/game/entities/enemy_config.cpp @@ -177,6 +177,79 @@ namespace { return true; } + 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_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& 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(); + 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) @@ -194,6 +267,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; } + if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; } return cfg; } catch (const std::exception& e) { diff --git a/source/game/entities/enemy_config.hpp b/source/game/entities/enemy_config.hpp index 8b6f152..a7ca422 100644 --- a/source/game/entities/enemy_config.hpp +++ b/source/game/entities/enemy_config.hpp @@ -14,6 +14,7 @@ #include "external/fkyaml_node.hpp" #include "game/entities/enemy.hpp" // EnemyType +#include "game/entities/enemy_event.hpp" struct EnemyConfig { struct ShapeCfg { @@ -98,6 +99,7 @@ struct EnemyConfig { 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). diff --git a/source/game/entities/enemy_event.hpp b/source/game/entities/enemy_event.hpp new file mode 100644 index 0000000..aa645fd --- /dev/null +++ b/source/game/entities/enemy_event.hpp @@ -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 +#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) +}; + +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 on_hit; + std::vector on_hurt_end; + std::vector on_destroy; + + [[nodiscard]] auto getActions(EnemyEventType event) const -> const std::vector& { + 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 + } +}; diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index d8995f4..dd5f302 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -9,61 +9,11 @@ #include "core/types.hpp" #include "game/constants.hpp" #include "game/entities/bullet_config.hpp" -#include "game/entities/enemy_config.hpp" +#include "game/systems/enemy_event_dispatcher.hpp" namespace Systems::Collision { namespace { - constexpr uint8_t NO_SHOOTER = 0xFF; - - // Mata al enemy con explosión: floating score, debris con velocity heredada, - // sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador. - // CRUCIAL: leer velocity/datos ANTES de destruir() (que zera la velocity). - void explodeNow(Context& ctx, Enemy& enemy, uint8_t shooter_id) { - const Vec2 ENEMY_POS = enemy.getCenter(); - const Vec2 ENEMY_VEL = enemy.getVelocityVector(); - const float BRIGHTNESS = enemy.getBrightness(); - const auto SHAPE = enemy.getShape(); - - const int POINTS = enemy.getConfig().score; - const SDL_Color COLOR = enemy.getConfig().colors.normal; - const SDL_Color WOUNDED_COLOR = enemy.getConfig().colors.wounded; - - if (shooter_id != NO_SHOOTER) { - ctx.score_per_player[shooter_id] += POINTS; - } - ctx.floating_score_manager.crear(POINTS, ENEMY_POS); - - enemy.destroy(); - - constexpr float SPEED_EXPLOSIO = 80.0F; // px/s (explosión suave) - const Vec2 INHERITED_VEL = ENEMY_VEL * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE; - ctx.debris_manager.explode( - SHAPE, - ENEMY_POS, - 0.0F, // angle (rotación interna del enemy) - 1.0F, // escala - SPEED_EXPLOSIO, - BRIGHTNESS, - INHERITED_VEL, - 0.0F, // sense herència angular: evita que els 5 trossos curvin en bloc - 0.0F, // sin herencia visual - Defaults::Sound::EXPLOSION, - COLOR, - Defaults::Physics::Debris::ENEMY_LIFETIME, - Defaults::Physics::Debris::ENEMY_FRICTION, - Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER); - - // Firework burst radial des del centro de l'enemic (efecte adicional al debris). - // Línia blanca + halo daurat (WOUNDED) per a feel d'espurnes. - ctx.firework_manager.spawn(ENEMY_POS, - Defaults::FX::Firework::DEFAULT_COLOR, - Defaults::FX::Firework::SPEED, - Defaults::FX::Firework::N_POINTS, - Defaults::FX::Firework::INITIAL_BRIGHTNESS, - /*glow=*/true, - WOUNDED_COLOR); - } // Trenca una bala en debris (8 fragments de l'octàgon) + so HIT + desactiva. // S'invoca des de qualsevol desactivació de bala (impacte amb enemic, amb jugador, @@ -102,26 +52,11 @@ namespace Systems::Collision { } // *** 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 * bullet.getConfig().physics.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; } } } @@ -132,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()); } } diff --git a/source/game/systems/enemy_event_dispatcher.cpp b/source/game/systems/enemy_event_dispatcher.cpp new file mode 100644 index 0000000..21f37ef --- /dev/null +++ b/source/game/systems/enemy_event_dispatcher.cpp @@ -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 + +#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 diff --git a/source/game/systems/enemy_event_dispatcher.hpp b/source/game/systems/enemy_event_dispatcher.hpp new file mode 100644 index 0000000..723b6a6 --- /dev/null +++ b/source/game/systems/enemy_event_dispatcher.hpp @@ -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 + +#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