feat(enemy): sistema d'HP declaratiu i nou enemic big_pentagon
This commit is contained in:
@@ -16,3 +16,20 @@ 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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<Graphics::Shape>& 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<std::pair<Vec2, Vec2>>;
|
||||
// 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);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#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);
|
||||
|
||||
@@ -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,24 @@ 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<int>();
|
||||
}
|
||||
}
|
||||
|
||||
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_debris_partial") { return EnemyActionType::CREATE_DEBRIS_PARTIAL; }
|
||||
if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; }
|
||||
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 +352,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 +390,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 +430,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; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -11,18 +11,22 @@
|
||||
#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)
|
||||
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
|
||||
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 +35,7 @@ struct EnemyAction {
|
||||
|
||||
struct EnemyEventConfig {
|
||||
std::vector<EnemyAction> on_hit;
|
||||
std::vector<EnemyAction> on_no_health;
|
||||
std::vector<EnemyAction> on_hurt_end;
|
||||
std::vector<EnemyAction> on_destroy;
|
||||
|
||||
@@ -38,6 +43,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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -171,11 +171,12 @@ namespace StageSystem {
|
||||
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).
|
||||
// 'star' i 'big_pentagon' són opcionals per compatibilitat amb stages antics (default 0).
|
||||
dist.star = yaml.contains("star") ? yaml["star"].get_value<uint8_t>() : 0;
|
||||
dist.big_pentagon = yaml.contains("big_pentagon") ? yaml["big_pentagon"].get_value<uint8_t>() : 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;
|
||||
|
||||
@@ -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 canvia piece_scale
|
||||
// (1.0 = explosió completa, 0.3 = xip d'esquerda).
|
||||
void spawnDebrisForEnemy(Systems::Collision::Context& ctx, const Enemy& enemy, const Bullet* bullet, float piece_scale) {
|
||||
constexpr float SPEED_EXPLOSIO = 80.0F;
|
||||
const Vec2 INHERITED_VEL = enemy.getVelocityVector() *
|
||||
Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
|
||||
@@ -44,7 +47,8 @@ namespace Systems::EnemyEvents {
|
||||
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) {
|
||||
@@ -88,6 +92,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,7 +117,10 @@ namespace Systems::EnemyEvents {
|
||||
doAddScore(ctx, enemy, shooter_id);
|
||||
break;
|
||||
case EnemyActionType::CREATE_DEBRIS:
|
||||
doCreateDebris(ctx, enemy, bullet);
|
||||
spawnDebrisForEnemy(ctx, enemy, bullet, 1.0F);
|
||||
break;
|
||||
case EnemyActionType::CREATE_DEBRIS_PARTIAL:
|
||||
spawnDebrisForEnemy(ctx, enemy, bullet, Defaults::Enemies::Debris::PARTIAL_PIECE_SCALE);
|
||||
break;
|
||||
case EnemyActionType::CREATE_FIREWORKS:
|
||||
doCreateFireworks(ctx, enemy);
|
||||
@@ -116,6 +130,15 @@ namespace Systems::EnemyEvents {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user