Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c0502eefb | |||
| 9b3da3a6e7 | |||
| bc41169176 |
@@ -53,3 +53,16 @@ colors:
|
|||||||
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
|
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
|
||||||
|
|
||||||
score: 100
|
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
|
||||||
|
|||||||
@@ -53,3 +53,14 @@ colors:
|
|||||||
wounded: [255, 220, 60]
|
wounded: [255, 220, 60]
|
||||||
|
|
||||||
score: 200
|
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
|
||||||
|
|||||||
@@ -53,3 +53,14 @@ colors:
|
|||||||
wounded: [255, 220, 60]
|
wounded: [255, 220, 60]
|
||||||
|
|
||||||
score: 150
|
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
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: star
|
||||||
|
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
|
||||||
|
|
||||||
|
shape:
|
||||||
|
path: star_5.shp
|
||||||
|
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 5.0
|
||||||
|
speed: 35.0 # Mateixos paràmetres que pentagon (esquivador lent).
|
||||||
|
rotation_delta_min: 0.75
|
||||||
|
rotation_delta_max: 3.75
|
||||||
|
restitution: 1.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
behavior:
|
||||||
|
# Hereta el comportament de Pentagon (zigzag esquivador).
|
||||||
|
angle_change_max: 1.0
|
||||||
|
zigzag_prob_per_second: 0.8
|
||||||
|
|
||||||
|
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.0
|
||||||
|
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: 36.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 255, 0] # Groc estrella
|
||||||
|
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
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# bullet_double.shp - Bala anular (dos cercles concèntrics)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Dos octàgons concèntrics al centre (0,0):
|
||||||
|
# - Exterior: radi 4 (lleugerament més gran que la bala estàndard, radi 3)
|
||||||
|
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
|
||||||
|
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
|
||||||
|
|
||||||
|
name: bullet_double
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Cercle exterior (octàgon, radi 4)
|
||||||
|
polyline: 0,-4 2.83,-2.83 4,0 2.83,2.83 0,4 -2.83,2.83 -4,0 -2.83,-2.83 0,-4
|
||||||
|
|
||||||
|
# Cercle interior (octàgon, radi 2)
|
||||||
|
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# bullet_long.shp - Bala allargada (dos octàgons tangents + tapes superior i inferior)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Dos cercles (octàgons radi 3) tangents externament al punt (0,0), units
|
||||||
|
# per una línia horitzontal superior i una d'inferior. La silueta resultant
|
||||||
|
# és una càpsula amb la separació visible dels dos cercles al centre.
|
||||||
|
#
|
||||||
|
# Geometria:
|
||||||
|
# Centre octàgon esquerre: (-3, 0)
|
||||||
|
# Centre octàgon dret: ( 3, 0)
|
||||||
|
# Punt de tangència: ( 0, 0)
|
||||||
|
# Bounding radius natiu ≈ 6 (extrem horitzontal a x=±6).
|
||||||
|
|
||||||
|
name: bullet_long
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Octàgon esquerre (centre x=-3, radi 3)
|
||||||
|
polyline: -3,-3 -0.88,-2.12 0,0 -0.88,2.12 -3,3 -5.12,2.12 -6,0 -5.12,-2.12 -3,-3
|
||||||
|
|
||||||
|
# Octàgon dret (centre x=3, radi 3)
|
||||||
|
polyline: 3,-3 5.12,-2.12 6,0 5.12,2.12 3,3 0.88,2.12 0,0 0.88,-2.12 3,-3
|
||||||
|
|
||||||
|
# Tapa superior: uneix el cim de l'octàgon esquerre amb el del dret
|
||||||
|
polyline: -3,-3 3,-3
|
||||||
|
|
||||||
|
# Tapa inferior: uneix la base de l'octàgon esquerre amb la del dret
|
||||||
|
polyline: -3,3 3,3
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# star_5.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
|
||||||
|
# interiors (radi 7.64 = 20/φ² ≈ proporció àuria) per donar puntes esveltes.
|
||||||
|
# Vèrtex apuntant amunt (igual que enemy_pentagon).
|
||||||
|
#
|
||||||
|
# Sense línies interiors: una única polyline que recorre el perímetre.
|
||||||
|
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
|
||||||
|
|
||||||
|
name: star_5
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
polyline: 0,-20 4.49,-6.18 19.02,-6.18 7.27,2.36 11.76,16.18 0,7.64 -11.76,16.18 -7.27,2.36 -19.02,-6.18 -4.49,-6.18 0,-20
|
||||||
@@ -7,7 +7,7 @@ metadata:
|
|||||||
description: "Progressive difficulty curve from novice to expert"
|
description: "Progressive difficulty curve from novice to expert"
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
# STAGE 1: Tutorial - Mix de tots els tipus, velocitat lenta
|
# STAGE 1: Tutorial - Mix de tots 4 tipus al 25% per mostrar-los junts
|
||||||
- stage_id: 1
|
- stage_id: 1
|
||||||
total_enemies: 50
|
total_enemies: 50
|
||||||
spawn_config:
|
spawn_config:
|
||||||
@@ -15,9 +15,10 @@ stages:
|
|||||||
initial_delay: 0.3
|
initial_delay: 0.3
|
||||||
spawn_interval: 0.4
|
spawn_interval: 0.4
|
||||||
enemy_distribution:
|
enemy_distribution:
|
||||||
pentagon: 34
|
pentagon: 25
|
||||||
cuadrado: 33
|
cuadrado: 25
|
||||||
molinillo: 33
|
molinillo: 25
|
||||||
|
star: 25
|
||||||
difficulty_multipliers:
|
difficulty_multipliers:
|
||||||
speed_multiplier: 0.7
|
speed_multiplier: 0.7
|
||||||
rotation_multiplier: 0.8
|
rotation_multiplier: 0.8
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ void Enemy::update(float delta_time) {
|
|||||||
if (!isWounded()) {
|
if (!isWounded()) {
|
||||||
switch (type_) {
|
switch (type_) {
|
||||||
case EnemyType::PENTAGON:
|
case EnemyType::PENTAGON:
|
||||||
|
case EnemyType::STAR:
|
||||||
|
// STAR reusa el zigzag esquivador de Pentagon. Si en el futur
|
||||||
|
// vol comportament propi, separa-li el cas.
|
||||||
behaviorPentagon(delta_time);
|
behaviorPentagon(delta_time);
|
||||||
break;
|
break;
|
||||||
case EnemyType::SQUARE:
|
case EnemyType::SQUARE:
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
enum class EnemyType : uint8_t {
|
enum class EnemyType : uint8_t {
|
||||||
PENTAGON = 0, // Pentágono esquivador (zigzag)
|
PENTAGON = 0, // Pentágono esquivador (zigzag)
|
||||||
SQUARE = 1, // Square perseguidor (tracks ship)
|
SQUARE = 1, // Square perseguidor (tracks ship)
|
||||||
PINWHEEL = 2 // Molinillo agresivo (rápido, girando)
|
PINWHEEL = 2, // Molinillo agresivo (rápido, girando)
|
||||||
|
STAR = 3 // Estrella de 5 puntes (clone visual de Pentagon, comportament zigzag)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp.
|
// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp.
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ namespace {
|
|||||||
if (s == "pentagon") { return EnemyType::PENTAGON; }
|
if (s == "pentagon") { return EnemyType::PENTAGON; }
|
||||||
if (s == "square") { return EnemyType::SQUARE; }
|
if (s == "square") { return EnemyType::SQUARE; }
|
||||||
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
|
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
|
||||||
|
if (s == "star") { return EnemyType::STAR; }
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +177,79 @@ namespace {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_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<EnemyAction>& 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<std::string>();
|
||||||
|
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
|
} // namespace
|
||||||
|
|
||||||
auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
|
auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
|
||||||
@@ -193,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 (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; }
|
||||||
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
|
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
|
||||||
if (!parseScore(node, cfg.name, cfg.score)) { 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;
|
return cfg;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/entities/enemy.hpp" // EnemyType
|
#include "game/entities/enemy.hpp" // EnemyType
|
||||||
|
#include "game/entities/enemy_event.hpp"
|
||||||
|
|
||||||
struct EnemyConfig {
|
struct EnemyConfig {
|
||||||
struct ShapeCfg {
|
struct ShapeCfg {
|
||||||
@@ -98,6 +99,7 @@ struct EnemyConfig {
|
|||||||
SpawnCfg spawn;
|
SpawnCfg spawn;
|
||||||
ColorsCfg colors;
|
ColorsCfg colors;
|
||||||
int score;
|
int score;
|
||||||
|
EnemyEventConfig events;
|
||||||
|
|
||||||
// Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del
|
// 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).
|
// YAML coincideix amb el tipus que el caller espera (segons el directori).
|
||||||
|
|||||||
@@ -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 <cstdint>
|
||||||
|
#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)
|
||||||
|
};
|
||||||
|
|
||||||
|
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<EnemyAction> on_hit;
|
||||||
|
std::vector<EnemyAction> on_hurt_end;
|
||||||
|
std::vector<EnemyAction> on_destroy;
|
||||||
|
|
||||||
|
[[nodiscard]] auto getActions(EnemyEventType event) const -> const std::vector<EnemyAction>& {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
EnemyConfig EnemyRegistry::pentagon_config;
|
EnemyConfig EnemyRegistry::pentagon_config;
|
||||||
EnemyConfig EnemyRegistry::square_config;
|
EnemyConfig EnemyRegistry::square_config;
|
||||||
EnemyConfig EnemyRegistry::pinwheel_config;
|
EnemyConfig EnemyRegistry::pinwheel_config;
|
||||||
|
EnemyConfig EnemyRegistry::star_config;
|
||||||
bool EnemyRegistry::loaded = false;
|
bool EnemyRegistry::loaded = false;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -36,10 +37,11 @@ namespace {
|
|||||||
auto EnemyRegistry::loadAll() -> bool {
|
auto EnemyRegistry::loadAll() -> bool {
|
||||||
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
|
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
|
||||||
loadOne("square", EnemyType::SQUARE, square_config) &&
|
loadOne("square", EnemyType::SQUARE, square_config) &&
|
||||||
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config);
|
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config) &&
|
||||||
|
loadOne("star", EnemyType::STAR, star_config);
|
||||||
loaded = OK;
|
loaded = OK;
|
||||||
if (OK) {
|
if (OK) {
|
||||||
std::cout << "[EnemyRegistry] 3 configuracions d'enemic carregades.\n";
|
std::cout << "[EnemyRegistry] 4 configuracions d'enemic carregades.\n";
|
||||||
}
|
}
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,8 @@ auto EnemyRegistry::get(EnemyType type) -> const EnemyConfig& {
|
|||||||
return square_config;
|
return square_config;
|
||||||
case EnemyType::PINWHEEL:
|
case EnemyType::PINWHEEL:
|
||||||
return pinwheel_config;
|
return pinwheel_config;
|
||||||
|
case EnemyType::STAR:
|
||||||
|
return star_config;
|
||||||
}
|
}
|
||||||
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
|
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
|
||||||
std::exit(EXIT_FAILURE);
|
std::exit(EXIT_FAILURE);
|
||||||
|
|||||||
@@ -26,5 +26,6 @@ class EnemyRegistry {
|
|||||||
static EnemyConfig pentagon_config;
|
static EnemyConfig pentagon_config;
|
||||||
static EnemyConfig square_config;
|
static EnemyConfig square_config;
|
||||||
static EnemyConfig pinwheel_config;
|
static EnemyConfig pinwheel_config;
|
||||||
|
static EnemyConfig star_config;
|
||||||
static bool loaded;
|
static bool loaded;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -136,7 +136,10 @@ namespace StageSystem {
|
|||||||
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
|
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
|
||||||
return EnemyType::SQUARE;
|
return EnemyType::SQUARE;
|
||||||
}
|
}
|
||||||
return EnemyType::PINWHEEL;
|
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado + config_->distribucio.molinillo) {
|
||||||
|
return EnemyType::PINWHEEL;
|
||||||
|
}
|
||||||
|
return EnemyType::STAR;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
|
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace StageSystem {
|
|||||||
uint8_t pentagon; // 0-100
|
uint8_t pentagon; // 0-100
|
||||||
uint8_t cuadrado; // 0-100
|
uint8_t cuadrado; // 0-100
|
||||||
uint8_t molinillo; // 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
|
// Suma ha de ser 100, validat en StageLoader
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ namespace StageSystem {
|
|||||||
// el tipo; basta con confirmar que no es 0 (sentinela "sin asignar").
|
// el tipo; basta con confirmar que no es 0 (sentinela "sin asignar").
|
||||||
return stage_id >= 1 &&
|
return stage_id >= 1 &&
|
||||||
total_enemies > 0 && total_enemies <= 200 &&
|
total_enemies > 0 && total_enemies <= 200 &&
|
||||||
distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo == 100;
|
distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo + distribucio.star == 100;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,241 +19,243 @@
|
|||||||
|
|
||||||
namespace StageSystem {
|
namespace StageSystem {
|
||||||
|
|
||||||
auto StageLoader::load(const std::string& path) -> std::unique_ptr<StageSystemConfig> {
|
auto StageLoader::load(const std::string& path) -> std::unique_ptr<StageSystemConfig> {
|
||||||
try {
|
try {
|
||||||
// Normalize path: "data/stages/stages.yaml" → "stages/stages.yaml"
|
// Normalize path: "data/stages/stages.yaml" → "stages/stages.yaml"
|
||||||
std::string normalized = path;
|
std::string normalized = path;
|
||||||
if (normalized.starts_with("data/")) {
|
if (normalized.starts_with("data/")) {
|
||||||
normalized = normalized.substr(5);
|
normalized = normalized.substr(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load from resource system
|
// Load from resource system
|
||||||
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
||||||
if (data.empty()) {
|
if (data.empty()) {
|
||||||
std::cerr << "[StageLoader] Error: no es pot load " << normalized << '\n';
|
std::cerr << "[StageLoader] Error: no es pot load " << normalized << '\n';
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to string
|
|
||||||
std::string yaml_content(data.begin(), data.end());
|
|
||||||
std::stringstream stream(yaml_content);
|
|
||||||
|
|
||||||
// Parse YAML
|
|
||||||
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
|
||||||
auto config = std::make_unique<StageSystemConfig>();
|
|
||||||
|
|
||||||
// Parse metadata
|
|
||||||
if (!yaml.contains("metadata")) {
|
|
||||||
std::cerr << "[StageLoader] Error: falta camp 'metadata'" << '\n';
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (!parseMetadata(yaml["metadata"], config->metadata)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse stages
|
|
||||||
if (!yaml.contains("stages")) {
|
|
||||||
std::cerr << "[StageLoader] Error: falta camp 'stages'" << '\n';
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!yaml["stages"].is_sequence()) {
|
|
||||||
std::cerr << "[StageLoader] Error: 'stages' ha de ser una list" << '\n';
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& stage_yaml : yaml["stages"]) {
|
|
||||||
StageConfig stage;
|
|
||||||
if (!parseStage(stage_yaml, stage)) {
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
config->stages.push_back(stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validar configuración
|
// Convert to string
|
||||||
if (!validateConfig(*config)) {
|
std::string yaml_content(data.begin(), data.end());
|
||||||
|
std::stringstream stream(yaml_content);
|
||||||
|
|
||||||
|
// Parse YAML
|
||||||
|
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
||||||
|
auto config = std::make_unique<StageSystemConfig>();
|
||||||
|
|
||||||
|
// Parse metadata
|
||||||
|
if (!yaml.contains("metadata")) {
|
||||||
|
std::cerr << "[StageLoader] Error: falta camp 'metadata'" << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!parseMetadata(yaml["metadata"], config->metadata)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse stages
|
||||||
|
if (!yaml.contains("stages")) {
|
||||||
|
std::cerr << "[StageLoader] Error: falta camp 'stages'" << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!yaml["stages"].is_sequence()) {
|
||||||
|
std::cerr << "[StageLoader] Error: 'stages' ha de ser una list" << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& stage_yaml : yaml["stages"]) {
|
||||||
|
StageConfig stage;
|
||||||
|
if (!parseStage(stage_yaml, stage)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
config->stages.push_back(stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar configuración
|
||||||
|
if (!validateConfig(*config)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[StageLoader] Carregats " << config->stages.size()
|
||||||
|
<< " stages correctament" << '\n';
|
||||||
|
return config;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[StageLoader] Excepció: " << e.what() << '\n';
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "[StageLoader] Carregats " << config->stages.size()
|
|
||||||
<< " stages correctament" << '\n';
|
|
||||||
return config;
|
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[StageLoader] Excepció: " << e.what() << '\n';
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
auto StageLoader::parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool {
|
auto StageLoader::parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool {
|
||||||
try {
|
try {
|
||||||
if (!yaml.contains("version") || !yaml.contains("total_stages")) {
|
if (!yaml.contains("version") || !yaml.contains("total_stages")) {
|
||||||
std::cerr << "[StageLoader] Error: metadata incompleta" << '\n';
|
std::cerr << "[StageLoader] Error: metadata incompleta" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.version = yaml["version"].get_value<std::string>();
|
||||||
|
meta.total_stages = yaml["total_stages"].get_value<uint8_t>();
|
||||||
|
meta.descripcio = yaml.contains("description")
|
||||||
|
? yaml["description"].get_value<std::string>()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[StageLoader] Error parsing metadata: " << e.what() << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
meta.version = yaml["version"].get_value<std::string>();
|
|
||||||
meta.total_stages = yaml["total_stages"].get_value<uint8_t>();
|
|
||||||
meta.descripcio = yaml.contains("description")
|
|
||||||
? yaml["description"].get_value<std::string>()
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[StageLoader] Error parsing metadata: " << e.what() << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool {
|
auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool {
|
||||||
try {
|
try {
|
||||||
if (!yaml.contains("stage_id") || !yaml.contains("total_enemies") ||
|
if (!yaml.contains("stage_id") || !yaml.contains("total_enemies") ||
|
||||||
!yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") ||
|
!yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") ||
|
||||||
!yaml.contains("difficulty_multipliers")) {
|
!yaml.contains("difficulty_multipliers")) {
|
||||||
std::cerr << "[StageLoader] Error: stage incompleta" << '\n';
|
std::cerr << "[StageLoader] Error: stage incompleta" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stage.stage_id = yaml["stage_id"].get_value<uint8_t>();
|
||||||
|
stage.total_enemies = yaml["total_enemies"].get_value<uint8_t>();
|
||||||
|
|
||||||
|
if (!parseSpawnConfig(yaml["spawn_config"], stage.config_spawn)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseDistribution(yaml["enemy_distribution"], stage.distribucio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseMultipliers(yaml["difficulty_multipliers"], stage.multiplicadors)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stage.isValid()) {
|
||||||
|
std::cerr << "[StageLoader] Error: stage " << static_cast<int>(stage.stage_id)
|
||||||
|
<< " no es vàlid" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[StageLoader] Error parsing stage: " << e.what() << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
stage.stage_id = yaml["stage_id"].get_value<uint8_t>();
|
|
||||||
stage.total_enemies = yaml["total_enemies"].get_value<uint8_t>();
|
|
||||||
|
|
||||||
if (!parseSpawnConfig(yaml["spawn_config"], stage.config_spawn)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseDistribution(yaml["enemy_distribution"], stage.distribucio)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseMultipliers(yaml["difficulty_multipliers"], stage.multiplicadors)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stage.isValid()) {
|
|
||||||
std::cerr << "[StageLoader] Error: stage " << static_cast<int>(stage.stage_id)
|
|
||||||
<< " no es vàlid" << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[StageLoader] Error parsing stage: " << e.what() << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool {
|
auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool {
|
||||||
try {
|
try {
|
||||||
if (!yaml.contains("mode") || !yaml.contains("initial_delay") ||
|
if (!yaml.contains("mode") || !yaml.contains("initial_delay") ||
|
||||||
!yaml.contains("spawn_interval")) {
|
!yaml.contains("spawn_interval")) {
|
||||||
std::cerr << "[StageLoader] Error: spawn_config incompleta" << '\n';
|
std::cerr << "[StageLoader] Error: spawn_config incompleta" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mode_str = yaml["mode"].get_value<std::string>();
|
||||||
|
config.mode = parseSpawnMode(mode_str);
|
||||||
|
config.delay_inicial = yaml["initial_delay"].get_value<float>();
|
||||||
|
config.interval_spawn = yaml["spawn_interval"].get_value<float>();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto mode_str = yaml["mode"].get_value<std::string>();
|
|
||||||
config.mode = parseSpawnMode(mode_str);
|
|
||||||
config.delay_inicial = yaml["initial_delay"].get_value<float>();
|
|
||||||
config.interval_spawn = yaml["spawn_interval"].get_value<float>();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool {
|
auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool {
|
||||||
try {
|
try {
|
||||||
if (!yaml.contains("pentagon") || !yaml.contains("cuadrado") ||
|
if (!yaml.contains("pentagon") || !yaml.contains("cuadrado") ||
|
||||||
!yaml.contains("molinillo")) {
|
!yaml.contains("molinillo")) {
|
||||||
std::cerr << "[StageLoader] Error: enemy_distribution incompleta" << '\n';
|
std::cerr << "[StageLoader] Error: enemy_distribution incompleta" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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).
|
||||||
|
dist.star = yaml.contains("star") ? yaml["star"].get_value<uint8_t>() : 0;
|
||||||
|
|
||||||
|
// Validar que suma 100
|
||||||
|
int sum = dist.pentagon + dist.cuadrado + dist.molinillo + dist.star;
|
||||||
|
if (sum != 100) {
|
||||||
|
std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
dist.pentagon = yaml["pentagon"].get_value<uint8_t>();
|
|
||||||
dist.cuadrado = yaml["cuadrado"].get_value<uint8_t>();
|
|
||||||
dist.molinillo = yaml["molinillo"].get_value<uint8_t>();
|
|
||||||
|
|
||||||
// Validar que suma 100
|
|
||||||
int sum = dist.pentagon + dist.cuadrado + dist.molinillo;
|
|
||||||
if (sum != 100) {
|
|
||||||
std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool {
|
auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool {
|
||||||
try {
|
try {
|
||||||
if (!yaml.contains("speed_multiplier") || !yaml.contains("rotation_multiplier") ||
|
if (!yaml.contains("speed_multiplier") || !yaml.contains("rotation_multiplier") ||
|
||||||
!yaml.contains("tracking_strength")) {
|
!yaml.contains("tracking_strength")) {
|
||||||
std::cerr << "[StageLoader] Error: difficulty_multipliers incompleta" << '\n';
|
std::cerr << "[StageLoader] Error: difficulty_multipliers incompleta" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mult.velocity = yaml["speed_multiplier"].get_value<float>();
|
||||||
|
mult.rotation = yaml["rotation_multiplier"].get_value<float>();
|
||||||
|
mult.tracking_strength = yaml["tracking_strength"].get_value<float>();
|
||||||
|
|
||||||
|
// Validar rangs raonables
|
||||||
|
if (mult.velocity < 0.1F || mult.velocity > 5.0F) {
|
||||||
|
std::cerr << "[StageLoader] Warning: speed_multiplier fuera de rang (0.1-5.0)" << '\n';
|
||||||
|
}
|
||||||
|
if (mult.rotation < 0.1F || mult.rotation > 5.0F) {
|
||||||
|
std::cerr << "[StageLoader] Warning: rotation_multiplier fuera de rang (0.1-5.0)" << '\n';
|
||||||
|
}
|
||||||
|
if (mult.tracking_strength < 0.0F || mult.tracking_strength > 2.0F) {
|
||||||
|
std::cerr << "[StageLoader] Warning: tracking_strength fuera de rang (0.0-2.0)" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[StageLoader] Error parsing multipliers: " << e.what() << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mult.velocity = yaml["speed_multiplier"].get_value<float>();
|
|
||||||
mult.rotation = yaml["rotation_multiplier"].get_value<float>();
|
|
||||||
mult.tracking_strength = yaml["tracking_strength"].get_value<float>();
|
|
||||||
|
|
||||||
// Validar rangs raonables
|
|
||||||
if (mult.velocity < 0.1F || mult.velocity > 5.0F) {
|
|
||||||
std::cerr << "[StageLoader] Warning: speed_multiplier fuera de rang (0.1-5.0)" << '\n';
|
|
||||||
}
|
|
||||||
if (mult.rotation < 0.1F || mult.rotation > 5.0F) {
|
|
||||||
std::cerr << "[StageLoader] Warning: rotation_multiplier fuera de rang (0.1-5.0)" << '\n';
|
|
||||||
}
|
|
||||||
if (mult.tracking_strength < 0.0F || mult.tracking_strength > 2.0F) {
|
|
||||||
std::cerr << "[StageLoader] Warning: tracking_strength fuera de rang (0.0-2.0)" << '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[StageLoader] Error parsing multipliers: " << e.what() << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn {
|
auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn {
|
||||||
if (mode_str == "progressive") {
|
if (mode_str == "progressive") {
|
||||||
|
return ModeSpawn::PROGRESSIVE;
|
||||||
|
}
|
||||||
|
if (mode_str == "immediate") {
|
||||||
|
return ModeSpawn::IMMEDIATE;
|
||||||
|
}
|
||||||
|
if (mode_str == "wave") {
|
||||||
|
return ModeSpawn::WAVE;
|
||||||
|
}
|
||||||
|
std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str
|
||||||
|
<< "', usant PROGRESSIVE" << '\n';
|
||||||
return ModeSpawn::PROGRESSIVE;
|
return ModeSpawn::PROGRESSIVE;
|
||||||
}
|
}
|
||||||
if (mode_str == "immediate") {
|
|
||||||
return ModeSpawn::IMMEDIATE;
|
|
||||||
}
|
|
||||||
if (mode_str == "wave") {
|
|
||||||
return ModeSpawn::WAVE;
|
|
||||||
}
|
|
||||||
std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str
|
|
||||||
<< "', usant PROGRESSIVE" << '\n';
|
|
||||||
return ModeSpawn::PROGRESSIVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool {
|
auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool {
|
||||||
if (config.stages.empty()) {
|
if (config.stages.empty()) {
|
||||||
std::cerr << "[StageLoader] Error: sin stage carregat" << '\n';
|
std::cerr << "[StageLoader] Error: sin stage carregat" << '\n';
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.stages.size() != config.metadata.total_stages) {
|
|
||||||
std::cerr << "[StageLoader] Warning: nombre de stages (" << config.stages.size()
|
|
||||||
<< ") no coincideix con metadata.total_stages ("
|
|
||||||
<< static_cast<int>(config.metadata.total_stages) << ")" << '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validar stage_id consecutius
|
|
||||||
for (size_t i = 0; i < config.stages.size(); i++) {
|
|
||||||
if (config.stages[i].stage_id != i + 1) {
|
|
||||||
std::cerr << "[StageLoader] Error: stage_id no consecutius (esperat "
|
|
||||||
<< i + 1 << ", trobat " << static_cast<int>(config.stages[i].stage_id)
|
|
||||||
<< ")" << '\n';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.stages.size() != config.metadata.total_stages) {
|
||||||
|
std::cerr << "[StageLoader] Warning: nombre de stages (" << config.stages.size()
|
||||||
|
<< ") no coincideix con metadata.total_stages ("
|
||||||
|
<< static_cast<int>(config.metadata.total_stages) << ")" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar stage_id consecutius
|
||||||
|
for (size_t i = 0; i < config.stages.size(); i++) {
|
||||||
|
if (config.stages[i].stage_id != i + 1) {
|
||||||
|
std::cerr << "[StageLoader] Error: stage_id no consecutius (esperat "
|
||||||
|
<< i + 1 << ", trobat " << static_cast<int>(config.stages[i].stage_id)
|
||||||
|
<< ")" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace StageSystem
|
} // namespace StageSystem
|
||||||
|
|||||||
@@ -9,61 +9,11 @@
|
|||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
#include "game/constants.hpp"
|
#include "game/constants.hpp"
|
||||||
#include "game/entities/bullet_config.hpp"
|
#include "game/entities/bullet_config.hpp"
|
||||||
#include "game/entities/enemy_config.hpp"
|
#include "game/systems/enemy_event_dispatcher.hpp"
|
||||||
|
|
||||||
namespace Systems::Collision {
|
namespace Systems::Collision {
|
||||||
|
|
||||||
namespace {
|
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.
|
// 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,
|
// S'invoca des de qualsevol desactivació de bala (impacte amb enemic, amb jugador,
|
||||||
@@ -102,26 +52,11 @@ namespace Systems::Collision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// *** COLISIÓN bullet → enemy ***
|
// *** COLISIÓN bullet → enemy ***
|
||||||
// Empuje físico cuasi-realista: el impulse és el moment de la bala
|
// La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu
|
||||||
// (m·v) multiplicat pel factor de transferència. Direcció = vector
|
// al YAML de l'enemic via la secció `events:`. Aquí només dispatchem.
|
||||||
// velocity de la bala (cap a on viatjava).
|
Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, bullet.getOwnerId(), &bullet);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
breakBullet(ctx.debris_manager, bullet);
|
breakBullet(ctx.debris_manager, bullet);
|
||||||
break; // Una bala impacta a un enemy y muere
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +67,7 @@ namespace Systems::Collision {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
enemy.consumeWoundExpired();
|
enemy.consumeWoundExpired();
|
||||||
explodeNow(ctx, enemy, enemy.getLastHitBy());
|
Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HURT_END, enemy.getLastHitBy());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 <cstdint>
|
||||||
|
|
||||||
|
#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
|
||||||
@@ -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 <cstdint>
|
||||||
|
|
||||||
|
#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
|
||||||
Reference in New Issue
Block a user