feat(orb): contra-atac amb bullet_double dirigida al jugador en rebre impacte
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AimMode>;
|
||||
|
||||
auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> {
|
||||
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<std::string>();
|
||||
}
|
||||
if (item.contains("bullet_speed")) {
|
||||
action.bullet_speed = item["bullet_speed"].get_value<float>();
|
||||
}
|
||||
if (item.contains("aim_mode")) {
|
||||
const auto AIM_STR = item["aim_mode"].get_value<std::string>();
|
||||
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<float>();
|
||||
}
|
||||
}
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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 {
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
|
||||
#include "game/systems/enemy_event_dispatcher.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
#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<float>(std::rand()) / static_cast<float>(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<uint8_t>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user