From 885caa6bc38162c2aa7b0ac28af65a2f0b8e9413 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 26 May 2026 18:53:34 +0200 Subject: [PATCH] feat(orb): contra-atac amb bullet_double dirigida al jugador en rebre impacte --- .../entities/bullet_double/bullet_double.yaml | 21 +++++ data/entities/orb/orb.yaml | 8 +- data/stages/stages.yaml | 3 +- source/game/entities/bullet.cpp | 2 +- source/game/entities/enemy_config.cpp | 49 ++++++++-- source/game/entities/enemy_event.hpp | 11 +++ .../game/systems/enemy_event_dispatcher.cpp | 90 +++++++++++++++++++ 7 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 data/entities/bullet_double/bullet_double.yaml diff --git a/data/entities/bullet_double/bullet_double.yaml b/data/entities/bullet_double/bullet_double.yaml new file mode 100644 index 0000000..ab8ce0f --- /dev/null +++ b/data/entities/bullet_double/bullet_double.yaml @@ -0,0 +1,21 @@ +name: bullet_double + +# Variant de bala "anular" (dos cercles concèntrics, aspecte d'aura de plasma). +# Pensada per a contra-atacs d'enemic (ex: orb dispara una bullet_double al +# jugador quan rep un impacte). Mateixa física que la bala bàsica del player; +# canvien la forma (cercle doble) i el color per llegir-se com a tret enemic +# distintiu (groc verdós vs. el verd laser del player o el roig de bullet_long). +shape: + path: bullet/double.shp + scale: 1.5 + collision_factor: 1.0 + +physics: + mass: 0.5 + restitution: 0.0 + linear_damping: 0.0 + angular_damping: 0.0 + impact_momentum_factor: 4.0 + +colors: + normal: [200, 255, 80] # groc verdós (chartreuse) — contra-atac de l'orb diff --git a/data/entities/orb/orb.yaml b/data/entities/orb/orb.yaml index a06673f..522f2be 100644 --- a/data/entities/orb/orb.yaml +++ b/data/entities/orb/orb.yaml @@ -55,7 +55,7 @@ spawn: 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. + normal: [255, 140, 110] # taronja rosat (coral) — distintiu del boss orb. wounded: [255, 220, 60] score: 500 # 5x un enemic normal: aguanta 10x més. @@ -69,10 +69,14 @@ health: 10 events: on_hit: + - action: fire_bullet # contra-atac: dispara bullet_double dirigida al jugador + bullet: bullet_double + bullet_speed: 200.0 + aim_mode: aimed - 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: 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 diff --git a/data/stages/stages.yaml b/data/stages/stages.yaml index ccd9562..ef2c583 100644 --- a/data/stages/stages.yaml +++ b/data/stages/stages.yaml @@ -18,10 +18,11 @@ metadata: stages: # STAGE 1 — Tutorial: contacte amb pentagons i un cuadrado. + # (Test: també hi ha un orb a la primera onada per provar el contra-atac.) - stage_id: 1 multipliers: { velocity: 0.85, rotation: 0.9, tracking: 0.3 } waves: - - spawn: [pentagon, pentagon] + - spawn: [pentagon, pentagon, orb] spawn_interval: 0.6 next: all_dead - spawn: [pentagon, pentagon, square] diff --git a/source/game/entities/bullet.cpp b/source/game/entities/bullet.cpp index ed92684..30fdf65 100644 --- a/source/game/entities/bullet.cpp +++ b/source/game/entities/bullet.cpp @@ -114,6 +114,6 @@ void Bullet::desactivar() { void Bullet::draw() const { if (is_active_ && shape_) { - Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, config_->colors.normal); + Rendering::renderShape(renderer_, shape_, center_, angle_, config_->shape.scale, 1.0F, brightness_, config_->colors.normal); } } diff --git a/source/game/entities/enemy_config.cpp b/source/game/entities/enemy_config.cpp index afb5654..0459c1f 100644 --- a/source/game/entities/enemy_config.cpp +++ b/source/game/entities/enemy_config.cpp @@ -186,6 +186,11 @@ namespace { } } + // Forward-decl: aimModeFromString viu més avall (junt amb la resta de + // helpers d'AI) però parseActionList el necessita per al payload de + // FIRE_BULLET. Evita reordenar tot el bloc. + auto aimModeFromString(const std::string& s) -> std::optional; + auto actionTypeFromString(const std::string& s) -> std::optional { if (s == "set_hurt") { return EnemyActionType::SET_HURT; } if (s == "destroy") { return EnemyActionType::DESTROY; } @@ -197,6 +202,7 @@ namespace { if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; } if (s == "decrease_health") { return EnemyActionType::DECREASE_HEALTH; } if (s == "flash") { return EnemyActionType::FLASH; } + if (s == "fire_bullet") { return EnemyActionType::FIRE_BULLET; } return std::nullopt; } @@ -219,19 +225,50 @@ namespace { << event_name << " (" << enemy_name << ")\n"; return false; } - out.push_back({*PARSED}); + EnemyAction action; + action.type = *PARSED; + // Payload de FIRE_BULLET. Camps opcionals; els defaults són els del struct. + if (action.type == EnemyActionType::FIRE_BULLET) { + if (item.contains("bullet")) { + action.bullet_config_name = item["bullet"].get_value(); + } + if (item.contains("bullet_speed")) { + action.bullet_speed = item["bullet_speed"].get_value(); + } + if (item.contains("aim_mode")) { + const auto AIM_STR = item["aim_mode"].get_value(); + const auto AIM = aimModeFromString(AIM_STR); + if (!AIM) { + std::cerr << "[EnemyConfig] Error: aim_mode desconegut '" << AIM_STR + << "' a " << event_name << " (" << enemy_name << ")\n"; + return false; + } + action.aim_mode = *AIM; + } + if (item.contains("jitter_rad")) { + action.jitter_rad = item["jitter_rad"].get_value(); + } + } + out.push_back(action); } return true; } // Defaults: replica el flux hardcoded actual (set_hurt → destroy → score+debris+fireworks). + // Construïm via mutació per esquivar warnings de designated-init parcial sobre + // l'EnemyAction (que té payload de FIRE_BULLET no rellevant ací). void fillLegacyDefaults(EnemyEventConfig& events) { - events.on_hit = {{EnemyActionType::SET_HURT}}; - events.on_hurt_end = {{EnemyActionType::DESTROY}}; + const auto MAKE = [](EnemyActionType type) { + EnemyAction a; + a.type = type; + return a; + }; + events.on_hit = {MAKE(EnemyActionType::SET_HURT)}; + events.on_hurt_end = {MAKE(EnemyActionType::DESTROY)}; events.on_destroy = { - {EnemyActionType::ADD_SCORE}, - {EnemyActionType::CREATE_DEBRIS}, - {EnemyActionType::CREATE_FIREWORKS}, + MAKE(EnemyActionType::ADD_SCORE), + MAKE(EnemyActionType::CREATE_DEBRIS), + MAKE(EnemyActionType::CREATE_FIREWORKS), }; } diff --git a/source/game/entities/enemy_event.hpp b/source/game/entities/enemy_event.hpp index 4c6e58b..761c1dd 100644 --- a/source/game/entities/enemy_event.hpp +++ b/source/game/entities/enemy_event.hpp @@ -8,8 +8,11 @@ #pragma once #include +#include #include +#include "game/entities/enemy_ai.hpp" // AimMode + enum class EnemyEventType : uint8_t { ON_HIT, // Impactat per una bala ON_NO_HEALTH, // health ha arribat a 0 o menys aquest frame (via DECREASE_HEALTH) @@ -28,10 +31,18 @@ enum class EnemyActionType : uint8_t { 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) + FIRE_BULLET, // Dispara una bala (config per nom) dirigida o aleatòria }; struct EnemyAction { EnemyActionType type; + + // Payload de FIRE_BULLET (ignorat per altres tipus). Paral·lel a AiTickAction + // perquè un futur refactor pugui compartir doShoot/doFireBullet si val la pena. + std::string bullet_config_name; + float bullet_speed{200.0F}; + AimMode aim_mode{AimMode::RANDOM}; + float jitter_rad{0.0F}; }; struct EnemyEventConfig { diff --git a/source/game/systems/enemy_event_dispatcher.cpp b/source/game/systems/enemy_event_dispatcher.cpp index 6e3bbe4..e4f6ec2 100644 --- a/source/game/systems/enemy_event_dispatcher.cpp +++ b/source/game/systems/enemy_event_dispatcher.cpp @@ -3,13 +3,19 @@ #include "game/systems/enemy_event_dispatcher.hpp" +#include #include +#include #include "core/defaults.hpp" #include "core/types.hpp" +#include "game/constants.hpp" #include "game/entities/bullet.hpp" #include "game/entities/bullet_config.hpp" +#include "game/entities/bullet_registry.hpp" +#include "game/entities/enemy_ai.hpp" #include "game/entities/enemy_config.hpp" +#include "game/entities/ship.hpp" namespace Systems::EnemyEvents { @@ -72,6 +78,87 @@ namespace Systems::EnemyEvents { (bullet->getBody().mass * bullet->getConfig().physics.impact_momentum_factor); enemy.applyImpulse(IMPULSE); } + + auto randFloat01() -> float { + return static_cast(std::rand()) / static_cast(RAND_MAX); + } + + // Còpia local de la mateixa primitiva que viu a enemy_ai_system.cpp. + // No s'ha extret a un header compartit perquè és l'únic punt de + // duplicació; si apareix un tercer consumidor, refactoritzar. + auto findNearestShipPosition(const Enemy& enemy) -> const Vec2* { + const Vec2& self = enemy.getCenter(); + const Vec2* best = nullptr; + float best_dist_sq = 0.0F; + for (const Ship* ship : enemy.getShips()) { + if (ship == nullptr || !ship->isActive()) { + continue; + } + const Vec2& pos = ship->getCenter(); + const Vec2 DELTA = pos - self; + const float DIST_SQ = DELTA.lengthSquared(); + if (best == nullptr || DIST_SQ < best_dist_sq) { + best = &pos; + best_dist_sq = DIST_SQ; + } + } + return best; + } + + // FIRE_BULLET: paral·lel a doShoot() d'enemy_ai_system.cpp, però disparat + // per esdeveniment (típicament on_hit per a contra-atacs) en lloc de + // periòdicament. owner_id es deriva de l'índex dins ctx.enemies via + // aritmètica de punters (l'array és contigu). + void doFireBullet(Systems::Collision::Context& ctx, const Enemy& enemy, const EnemyAction& action) { + if (action.bullet_config_name.empty()) { + return; + } + const BulletConfig* cfg = BulletRegistry::get(action.bullet_config_name); + if (cfg == nullptr) { + return; + } + Bullet* slot = nullptr; + constexpr std::size_t START = Defaults::Entities::ENEMY_BULLET_START_IDX; + constexpr std::size_t END = START + Defaults::Entities::MAX_ENEMY_BULLETS; + for (std::size_t i = START; i < END; ++i) { + if (!ctx.bullets[i].isActive()) { + slot = &ctx.bullets[i]; + break; + } + } + if (slot == nullptr) { + return; // pool d'enemic ple + } + + float angle = 0.0F; + if (action.aim_mode == AimMode::AIMED) { + const Vec2* target = findNearestShipPosition(enemy); + if (target == nullptr) { + angle = randFloat01() * 2.0F * Constants::PI; + } else { + const Vec2 TO = *target - enemy.getCenter(); + angle = std::atan2(TO.y, TO.x) + (Constants::PI / 2.0F); + } + } else { + angle = randFloat01() * 2.0F * Constants::PI; + } + if (action.jitter_rad > 0.0F) { + angle += (randFloat01() - 0.5F) * 2.0F * action.jitter_rad; + } + + // Localitzem l'índex de l'enemic per construir l'owner_id. Evitem + // aritmètica de punters sobre Enemy (tipus polimòrfic — UB si la + // jerarquia canvia); cerca lineal a l'array (mida petita, no és hot path). + std::size_t enemy_index = 0; + for (std::size_t i = 0; i < ctx.enemies.size(); ++i) { + if (&ctx.enemies[i] == &enemy) { + enemy_index = i; + break; + } + } + const auto OWNER = static_cast(Defaults::Entities::ENEMY_OWNER_BASE + enemy_index); + slot->fire(enemy.getCenter(), angle, OWNER, action.bullet_speed, cfg); + } } // namespace void dispatchEvent(Systems::Collision::Context& ctx, Enemy& enemy, EnemyEventType event, uint8_t shooter_id, const Bullet* bullet) { @@ -153,6 +240,9 @@ namespace Systems::EnemyEvents { case EnemyActionType::FLASH: enemy.triggerFlash(); break; + case EnemyActionType::FIRE_BULLET: + doFireBullet(ctx, enemy, action); + break; } } }