feat(entities): derivar collision_radius del shape + scale/collision_factor al YAML

This commit is contained in:
2026-05-25 11:29:43 +02:00
parent da8eab330d
commit 866a057704
12 changed files with 149 additions and 104 deletions
+2 -1
View File
@@ -3,13 +3,14 @@ ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PE
shape: shape:
path: enemy_pentagon.shp path: enemy_pentagon.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics: physics:
mass: 5.0 mass: 5.0
speed: 35.0 # px/s (esquivador lent) speed: 35.0 # px/s (esquivador lent)
rotation_delta_min: 0.75 # rad/s — rotació visual mínima rotation_delta_min: 0.75 # rad/s — rotació visual mínima
rotation_delta_max: 3.75 # rad/s — rotació visual màxima rotation_delta_max: 3.75 # rad/s — rotació visual màxima
collision_radius: 20.0
behavior: behavior:
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon). # Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
+2 -1
View File
@@ -3,13 +3,14 @@ ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PI
shape: shape:
path: enemy_pinwheel.shp path: enemy_pinwheel.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics: physics:
mass: 4.0 # Més lleuger — àgil mass: 4.0 # Més lleuger — àgil
speed: 50.0 # px/s (el més ràpid) speed: 50.0 # px/s (el més ràpid)
rotation_delta_min: 3.0 # rad/s — rotació base elevada rotation_delta_min: 3.0 # rad/s — rotació base elevada
rotation_delta_max: 6.0 rotation_delta_max: 6.0
collision_radius: 20.0
behavior: behavior:
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau. # Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
+7 -1
View File
@@ -4,15 +4,21 @@ name: player_ship
# Nota: el segon jugador rep un override del shape ("ship2.shp") al ctor. # Nota: el segon jugador rep un override del shape ("ship2.shp") al ctor.
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat # Quan s'introdueixin variants reals de nau, es crearà un YAML separat
# per cada model. # per cada model.
#
# scale: multiplicador visual i de hitbox sobre la mida nativa del .shp (1.0 = mida del fitxer).
# collision_factor: ajust opcional del hitbox respecte el cercle circumscrit
# automàtic de la shape; tocar només si el feel del hitbox
# no quadra amb la silueta visual (default 1.0).
shape: shape:
path: ship.shp path: ship.shp
scale: 1.0
collision_factor: 1.0
physics: physics:
mass: 10.0 mass: 10.0
restitution: 0.6 restitution: 0.6
linear_damping: 1.5 linear_damping: 1.5
angular_damping: 0.0 angular_damping: 0.0
collision_radius: 12.0
rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia) rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia)
acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST
max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade) max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade)
+2 -1
View File
@@ -3,13 +3,14 @@ ai_type: square # Validat contra el directori; mapeja a EnemyType::SQ
shape: shape:
path: enemy_square.shp path: enemy_square.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics: physics:
mass: 8.0 # Més pesat — "tanc" mass: 8.0 # Més pesat — "tanc"
speed: 40.0 # px/s (velocitat mitjana) speed: 40.0 # px/s (velocitat mitjana)
rotation_delta_min: 0.3 # rad/s — rotació lenta rotation_delta_min: 0.3 # rad/s — rotació lenta
rotation_delta_max: 1.5 rotation_delta_max: 1.5
collision_radius: 20.0
behavior: behavior:
# Square: tracking discret cap a la nau cada N segons. # Square: tracking discret cap a la nau cada N segons.
+2 -1
View File
@@ -53,7 +53,8 @@ namespace Graphics {
// Cache and return // Cache and return
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName() std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName()
<< ", " << shape->getNumPrimitives() << " primitives)" << '\n'; << ", " << shape->getNumPrimitives() << " primitives, bounding_radius="
<< shape->getBoundingRadius() << ")" << '\n';
cache[filename] = shape; cache[filename] = shape;
return shape; return shape;
+9 -6
View File
@@ -55,22 +55,24 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
config_ = &EnemyRegistry::get(type); config_ = &EnemyRegistry::get(type);
const EnemyConfig& cfg = *config_; const EnemyConfig& cfg = *config_;
collision_radius_ = cfg.physics.collision_radius;
// Cas Square: resetejar tracking timer al spawn. // Cas Square: resetejar tracking timer al spawn.
if (type_ == EnemyType::SQUARE) { if (type_ == EnemyType::SQUARE) {
tracking_timer_ = 0.0F; tracking_timer_ = 0.0F;
tracking_strength_ = cfg.behavior.tracking_strength; tracking_strength_ = cfg.behavior.tracking_strength;
} }
body_.setMass(cfg.physics.mass);
body_.radius = collision_radius_;
shape_ = Graphics::ShapeLoader::load(cfg.shape.path); shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n'; std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n';
} }
// Radi de col·lisió derivat del cercle circumscrit de la shape * scale * collision_factor.
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
collision_radius_ = BOUNDING * cfg.shape.scale * cfg.shape.collision_factor;
body_.setMass(cfg.physics.mass);
body_.radius = collision_radius_;
// Posición aleatoria con comprobación de seguridad // Posición aleatoria con comprobación de seguridad
float min_x; float min_x;
float max_x; float max_x;
@@ -188,7 +190,8 @@ void Enemy::draw() const {
if (!is_active_ || !shape_) { if (!is_active_ || !shape_) {
return; return;
} }
const float SCALE = computeCurrentScale(); // El SCALE final = escala base del YAML * modulador dinàmic (spawn/pulse).
const float SCALE = config_->shape.scale * computeCurrentScale();
SDL_Color color = config_->colors.normal; SDL_Color color = config_->colors.normal;
// Parpadeo dorado mientras está herido. // Parpadeo dorado mientras está herido.
+96 -75
View File
@@ -32,89 +32,110 @@ namespace {
return std::nullopt; return std::nullopt;
} }
// Cada parseXxx valida + omple la sub-struct corresponent. Retornen false
// amb log si falta un camp requerit. Separar-los baixa la complexitat
// cognitiva del fromYaml() principal.
auto parseAiType(const fkyaml::node& node, EnemyType expected, const std::string& name, EnemyType& out) -> bool {
if (!node.contains("ai_type")) {
std::cerr << "[EnemyConfig] Error: falta 'ai_type' a " << name << '\n';
return false;
}
const auto AI_STR = node["ai_type"].get_value<std::string>();
const auto PARSED = aiTypeFromString(AI_STR);
if (!PARSED) {
std::cerr << "[EnemyConfig] Error: ai_type desconegut '" << AI_STR << "' a " << name << '\n';
return false;
}
if (*PARSED != expected) {
std::cerr << "[EnemyConfig] Error: ai_type '" << AI_STR
<< "' no coincideix amb el tipus esperat (per directori) a " << name << '\n';
return false;
}
out = *PARSED;
return true;
}
auto parseShape(const fkyaml::node& node, const std::string& name, EnemyConfig::ShapeCfg& out) -> bool {
if (!node.contains("shape") || !node["shape"].contains("path")) {
std::cerr << "[EnemyConfig] Error: falta 'shape.path' a " << name << '\n';
return false;
}
const auto& shape = node["shape"];
out.path = shape["path"].get_value<std::string>();
out.scale = shape.contains("scale") ? shape["scale"].get_value<float>() : 1.0F;
out.collision_factor = shape.contains("collision_factor")
? shape["collision_factor"].get_value<float>()
: 1.0F;
return true;
}
auto parsePhysics(const fkyaml::node& node, const std::string& name, EnemyConfig::PhysicsCfg& out) -> bool {
if (!node.contains("physics")) {
std::cerr << "[EnemyConfig] Error: falta 'physics' a " << name << '\n';
return false;
}
const auto& p = node["physics"];
out.mass = p["mass"].get_value<float>();
out.speed = p["speed"].get_value<float>();
out.rotation_delta_min = p["rotation_delta_min"].get_value<float>();
out.rotation_delta_max = p["rotation_delta_max"].get_value<float>();
return true;
}
// Tots els camps de behavior són opcionals; només l'AI corresponent els consumeix.
void parseBehavior(const fkyaml::node& node, EnemyConfig::BehaviorCfg& out) {
if (!node.contains("behavior")) {
return;
}
const auto& b = node["behavior"];
const auto READ_OPT = [&b](const char* key, float& dst) {
if (b.contains(key)) {
dst = b[key].get_value<float>();
}
};
READ_OPT("zigzag_prob_per_second", out.zigzag_prob_per_second);
READ_OPT("angle_change_max", out.angle_change_max);
READ_OPT("tracking_strength", out.tracking_strength);
READ_OPT("tracking_interval", out.tracking_interval);
READ_OPT("rotation_proximity_multiplier", out.rotation_proximity_multiplier);
READ_OPT("proximity_distance", out.proximity_distance);
}
auto parseColors(const fkyaml::node& node, const std::string& name, EnemyConfig::ColorsCfg& out) -> bool {
if (!node.contains("colors") ||
!parseColor(node["colors"]["normal"], out.normal) ||
!parseColor(node["colors"]["wounded"], out.wounded)) {
std::cerr << "[EnemyConfig] Error: 'colors.normal' / 'colors.wounded' no són [r,g,b] a "
<< name << '\n';
return false;
}
return true;
}
auto parseScore(const fkyaml::node& node, const std::string& name, int& out) -> bool {
if (!node.contains("score")) {
std::cerr << "[EnemyConfig] Error: falta 'score' a " << name << '\n';
return false;
}
out = node["score"].get_value<int>();
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)
-> std::optional<EnemyConfig> { -> std::optional<EnemyConfig> {
try { try {
EnemyConfig cfg; EnemyConfig cfg;
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "enemy"; cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "enemy";
// ai_type — validació estricta contra el tipus esperat if (!parseAiType(node, expected_ai_type, cfg.name, cfg.ai_type)) { return std::nullopt; }
if (!node.contains("ai_type")) { if (!parseShape(node, cfg.name, cfg.shape)) { return std::nullopt; }
std::cerr << "[EnemyConfig] Error: falta 'ai_type' a " << cfg.name << '\n'; if (!parsePhysics(node, cfg.name, cfg.physics)) { return std::nullopt; }
return std::nullopt; parseBehavior(node, cfg.behavior);
} if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
const auto AI_STR = node["ai_type"].get_value<std::string>(); if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
const auto PARSED = aiTypeFromString(AI_STR);
if (!PARSED) {
std::cerr << "[EnemyConfig] Error: ai_type desconegut '" << AI_STR << "' a " << cfg.name << '\n';
return std::nullopt;
}
if (*PARSED != expected_ai_type) {
std::cerr << "[EnemyConfig] Error: ai_type '" << AI_STR
<< "' no coincideix amb el tipus esperat (per directori) a " << cfg.name << '\n';
return std::nullopt;
}
cfg.ai_type = *PARSED;
// shape
if (!node.contains("shape") || !node["shape"].contains("path")) {
std::cerr << "[EnemyConfig] Error: falta 'shape.path' a " << cfg.name << '\n';
return std::nullopt;
}
cfg.shape.path = node["shape"]["path"].get_value<std::string>();
// physics
if (!node.contains("physics")) {
std::cerr << "[EnemyConfig] Error: falta 'physics' a " << cfg.name << '\n';
return std::nullopt;
}
const auto& physics = node["physics"];
cfg.physics.mass = physics["mass"].get_value<float>();
cfg.physics.speed = physics["speed"].get_value<float>();
cfg.physics.rotation_delta_min = physics["rotation_delta_min"].get_value<float>();
cfg.physics.rotation_delta_max = physics["rotation_delta_max"].get_value<float>();
cfg.physics.collision_radius = physics["collision_radius"].get_value<float>();
// behavior — tots els camps són opcionals; només l'AI corresponent els consumeix.
if (node.contains("behavior")) {
const auto& b = node["behavior"];
if (b.contains("zigzag_prob_per_second")) {
cfg.behavior.zigzag_prob_per_second = b["zigzag_prob_per_second"].get_value<float>();
}
if (b.contains("angle_change_max")) {
cfg.behavior.angle_change_max = b["angle_change_max"].get_value<float>();
}
if (b.contains("tracking_strength")) {
cfg.behavior.tracking_strength = b["tracking_strength"].get_value<float>();
}
if (b.contains("tracking_interval")) {
cfg.behavior.tracking_interval = b["tracking_interval"].get_value<float>();
}
if (b.contains("rotation_proximity_multiplier")) {
cfg.behavior.rotation_proximity_multiplier = b["rotation_proximity_multiplier"].get_value<float>();
}
if (b.contains("proximity_distance")) {
cfg.behavior.proximity_distance = b["proximity_distance"].get_value<float>();
}
}
// colors
if (!node.contains("colors") ||
!parseColor(node["colors"]["normal"], cfg.colors.normal) ||
!parseColor(node["colors"]["wounded"], cfg.colors.wounded)) {
std::cerr << "[EnemyConfig] Error: 'colors.normal' / 'colors.wounded' no són [r,g,b] a " << cfg.name << '\n';
return std::nullopt;
}
// score
if (!node.contains("score")) {
std::cerr << "[EnemyConfig] Error: falta 'score' a " << cfg.name << '\n';
return std::nullopt;
}
cfg.score = node["score"].get_value<int>();
return cfg; return cfg;
} catch (const std::exception& e) { } catch (const std::exception& e) {
+2 -1
View File
@@ -18,6 +18,8 @@
struct EnemyConfig { struct EnemyConfig {
struct ShapeCfg { struct ShapeCfg {
std::string path; std::string path;
float scale; // multiplicador visual + hitbox sobre la mida nativa del .shp
float collision_factor; // ajust opcional del hitbox respecte el cercle circumscrit (default 1.0)
}; };
struct PhysicsCfg { struct PhysicsCfg {
@@ -25,7 +27,6 @@ struct EnemyConfig {
float speed; float speed;
float rotation_delta_min; float rotation_delta_min;
float rotation_delta_max; float rotation_delta_max;
float collision_radius;
}; };
// Camps específics de cada AI. Els no aplicables a un tipus queden a 0.0F // Camps específics de cada AI. Els no aplicables a un tipus queden a 0.0F
+6 -3
View File
@@ -7,7 +7,6 @@
#include <exception> #include <exception>
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <vector>
namespace { namespace {
@@ -41,7 +40,12 @@ auto PlayerConfig::fromYaml(const fkyaml::node& node) -> std::optional<PlayerCon
std::cerr << "[PlayerConfig] Error: falta 'shape.path'" << '\n'; std::cerr << "[PlayerConfig] Error: falta 'shape.path'" << '\n';
return std::nullopt; return std::nullopt;
} }
cfg.shape.path = node["shape"]["path"].get_value<std::string>(); const auto& shape = node["shape"];
cfg.shape.path = shape["path"].get_value<std::string>();
cfg.shape.scale = shape.contains("scale") ? shape["scale"].get_value<float>() : 1.0F;
cfg.shape.collision_factor = shape.contains("collision_factor")
? shape["collision_factor"].get_value<float>()
: 1.0F;
// physics // physics
if (!node.contains("physics")) { if (!node.contains("physics")) {
@@ -53,7 +57,6 @@ auto PlayerConfig::fromYaml(const fkyaml::node& node) -> std::optional<PlayerCon
cfg.physics.restitution = physics["restitution"].get_value<float>(); cfg.physics.restitution = physics["restitution"].get_value<float>();
cfg.physics.linear_damping = physics["linear_damping"].get_value<float>(); cfg.physics.linear_damping = physics["linear_damping"].get_value<float>();
cfg.physics.angular_damping = physics["angular_damping"].get_value<float>(); cfg.physics.angular_damping = physics["angular_damping"].get_value<float>();
cfg.physics.collision_radius = physics["collision_radius"].get_value<float>();
cfg.physics.rotation_speed = physics["rotation_speed"].get_value<float>(); cfg.physics.rotation_speed = physics["rotation_speed"].get_value<float>();
cfg.physics.acceleration = physics["acceleration"].get_value<float>(); cfg.physics.acceleration = physics["acceleration"].get_value<float>();
cfg.physics.max_velocity = physics["max_velocity"].get_value<float>(); cfg.physics.max_velocity = physics["max_velocity"].get_value<float>();
+2 -1
View File
@@ -17,6 +17,8 @@
struct PlayerConfig { struct PlayerConfig {
struct ShapeCfg { struct ShapeCfg {
std::string path; std::string path;
float scale; // multiplicador visual + hitbox sobre la mida nativa del .shp
float collision_factor; // ajust opcional del hitbox respecte el cercle circumscrit (default 1.0)
}; };
struct PhysicsCfg { struct PhysicsCfg {
@@ -24,7 +26,6 @@ struct PlayerConfig {
float restitution; float restitution;
float linear_damping; float linear_damping;
float angular_damping; float angular_damping;
float collision_radius;
float rotation_speed; // rad/s float rotation_speed; // rad/s
float acceleration; // px/s^2 multiplicat per la massa float acceleration; // px/s^2 multiplicat per la massa
float max_velocity; // px/s (clamp post-integració) float max_velocity; // px/s (clamp post-integració)
+13 -8
View File
@@ -26,18 +26,22 @@ Ship::Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape
config_(std::move(config)) { config_(std::move(config)) {
brightness_ = Defaults::Brightness::NAU; brightness_ = Defaults::Brightness::NAU;
body_.setMass(config_.physics.mass);
body_.radius = config_.physics.collision_radius;
body_.restitution = config_.physics.restitution;
body_.linear_damping = config_.physics.linear_damping;
body_.angular_damping = config_.physics.angular_damping;
// El shape pot venir del YAML o ser overridden (ex: P2 amb "ship2.shp"). // El shape pot venir del YAML o ser overridden (ex: P2 amb "ship2.shp").
const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path; const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path;
shape_ = Graphics::ShapeLoader::load(SHAPE_PATH); shape_ = Graphics::ShapeLoader::load(SHAPE_PATH);
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
std::cerr << "[Ship] Error: no se ha podido cargar " << SHAPE_PATH << '\n'; std::cerr << "[Ship] Error: no se ha podido cargar " << SHAPE_PATH << '\n';
} }
// Radi de col·lisió derivat del cercle circumscrit de la shape * scale * collision_factor.
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
collision_radius_ = BOUNDING * config_.shape.scale * config_.shape.collision_factor;
body_.setMass(config_.physics.mass);
body_.radius = collision_radius_;
body_.restitution = config_.physics.restitution;
body_.linear_damping = config_.physics.linear_damping;
body_.angular_damping = config_.physics.angular_damping;
} }
void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) { void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) {
@@ -144,10 +148,11 @@ void Ship::draw() const {
return; return;
} }
// Efecto visual de empuje: escala proporcional a la velocidad. // Efecte visual d'empenta (modulador sobre l'escala base del YAML).
const float SPEED = getSpeed(); const float SPEED = getSpeed();
const float VISUAL_PUSH = SPEED / config_.visual_thrust.push_divisor; const float VISUAL_PUSH = SPEED / config_.visual_thrust.push_divisor;
const float SCALE = 1.0F + (VISUAL_PUSH / config_.visual_thrust.scale_divisor); const float THRUST_MODULATOR = 1.0F + (VISUAL_PUSH / config_.visual_thrust.scale_divisor);
const float SCALE = config_.shape.scale * THRUST_MODULATOR;
// Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt. // Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt.
SDL_Color color = config_.colors.normal; SDL_Color color = config_.colors.normal;
+6 -5
View File
@@ -6,7 +6,6 @@
#include <cstdint> #include <cstdint>
#include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/entities/player_config.hpp" #include "game/entities/player_config.hpp"
@@ -30,10 +29,9 @@ class Ship : public Entities::Entity {
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return !is_hit_; } [[nodiscard]] auto isActive() const -> bool override { return !is_hit_; }
// Override: Interfaz de colisión // Override: Interfaz de colisión. Derivat al ctor del bounding_radius del
[[nodiscard]] auto getCollisionRadius() const -> float override { // shape carregat × scale × collision_factor.
return config_.physics.collision_radius; [[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
}
[[nodiscard]] auto isCollidable() const -> bool override { [[nodiscard]] auto isCollidable() const -> bool override {
return !is_hit_ && invulnerable_timer_ <= 0.0F; return !is_hit_ && invulnerable_timer_ <= 0.0F;
} }
@@ -73,6 +71,9 @@ class Ship : public Entities::Entity {
// copy/move-assignment quan GameScene crea la nau real. // copy/move-assignment quan GameScene crea la nau real.
PlayerConfig config_{}; PlayerConfig config_{};
// Radi de col·lisió derivat: shape.bounding_radius × shape.scale × shape.collision_factor.
float collision_radius_{0.0F};
bool is_hit_{false}; bool is_hit_{false};
float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable