From 866a057704a3b629467493b89b8504480e1afc8b Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 25 May 2026 11:29:43 +0200 Subject: [PATCH] feat(entities): derivar collision_radius del shape + scale/collision_factor al YAML --- data/entities/pentagon/pentagon.yaml | 3 +- data/entities/pinwheel/pinwheel.yaml | 3 +- data/entities/player/player.yaml | 8 +- data/entities/square/square.yaml | 3 +- source/core/graphics/shape_loader.cpp | 3 +- source/game/entities/enemy.cpp | 15 ++- source/game/entities/enemy_config.cpp | 171 ++++++++++++++----------- source/game/entities/enemy_config.hpp | 3 +- source/game/entities/player_config.cpp | 9 +- source/game/entities/player_config.hpp | 3 +- source/game/entities/ship.cpp | 21 +-- source/game/entities/ship.hpp | 11 +- 12 files changed, 149 insertions(+), 104 deletions(-) diff --git a/data/entities/pentagon/pentagon.yaml b/data/entities/pentagon/pentagon.yaml index 328f8ef..c1d88e8 100644 --- a/data/entities/pentagon/pentagon.yaml +++ b/data/entities/pentagon/pentagon.yaml @@ -3,13 +3,14 @@ ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PE shape: 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: mass: 5.0 speed: 35.0 # px/s (esquivador lent) rotation_delta_min: 0.75 # rad/s — rotació visual mínima rotation_delta_max: 3.75 # rad/s — rotació visual màxima - collision_radius: 20.0 behavior: # Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon). diff --git a/data/entities/pinwheel/pinwheel.yaml b/data/entities/pinwheel/pinwheel.yaml index fe79082..2f7bb42 100644 --- a/data/entities/pinwheel/pinwheel.yaml +++ b/data/entities/pinwheel/pinwheel.yaml @@ -3,13 +3,14 @@ ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PI shape: 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: mass: 4.0 # Més lleuger — àgil speed: 50.0 # px/s (el més ràpid) rotation_delta_min: 3.0 # rad/s — rotació base elevada rotation_delta_max: 6.0 - collision_radius: 20.0 behavior: # Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau. diff --git a/data/entities/player/player.yaml b/data/entities/player/player.yaml index 58885c3..e318b37 100644 --- a/data/entities/player/player.yaml +++ b/data/entities/player/player.yaml @@ -4,15 +4,21 @@ name: player_ship # 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 # 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: path: ship.shp + scale: 1.0 + collision_factor: 1.0 physics: mass: 10.0 restitution: 0.6 linear_damping: 1.5 angular_damping: 0.0 - collision_radius: 12.0 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 max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade) diff --git a/data/entities/square/square.yaml b/data/entities/square/square.yaml index e781a76..7541453 100644 --- a/data/entities/square/square.yaml +++ b/data/entities/square/square.yaml @@ -3,13 +3,14 @@ ai_type: square # Validat contra el directori; mapeja a EnemyType::SQ shape: 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: mass: 8.0 # Més pesat — "tanc" speed: 40.0 # px/s (velocitat mitjana) rotation_delta_min: 0.3 # rad/s — rotació lenta rotation_delta_max: 1.5 - collision_radius: 20.0 behavior: # Square: tracking discret cap a la nau cada N segons. diff --git a/source/core/graphics/shape_loader.cpp b/source/core/graphics/shape_loader.cpp index 7f662a0..15d6159 100644 --- a/source/core/graphics/shape_loader.cpp +++ b/source/core/graphics/shape_loader.cpp @@ -53,7 +53,8 @@ namespace Graphics { // Cache and return std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName() - << ", " << shape->getNumPrimitives() << " primitives)" << '\n'; + << ", " << shape->getNumPrimitives() << " primitives, bounding_radius=" + << shape->getBoundingRadius() << ")" << '\n'; cache[filename] = shape; return shape; diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index 0e1a4cc..9e576a3 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -55,22 +55,24 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { config_ = &EnemyRegistry::get(type); const EnemyConfig& cfg = *config_; - collision_radius_ = cfg.physics.collision_radius; - // Cas Square: resetejar tracking timer al spawn. if (type_ == EnemyType::SQUARE) { tracking_timer_ = 0.0F; tracking_strength_ = cfg.behavior.tracking_strength; } - body_.setMass(cfg.physics.mass); - body_.radius = collision_radius_; - shape_ = Graphics::ShapeLoader::load(cfg.shape.path); if (!shape_ || !shape_->isValid()) { 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 float min_x; float max_x; @@ -188,7 +190,8 @@ void Enemy::draw() const { if (!is_active_ || !shape_) { 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; // Parpadeo dorado mientras está herido. diff --git a/source/game/entities/enemy_config.cpp b/source/game/entities/enemy_config.cpp index 509f5d9..c699391 100644 --- a/source/game/entities/enemy_config.cpp +++ b/source/game/entities/enemy_config.cpp @@ -32,89 +32,110 @@ namespace { 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(); + 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(); + out.scale = shape.contains("scale") ? shape["scale"].get_value() : 1.0F; + out.collision_factor = shape.contains("collision_factor") + ? shape["collision_factor"].get_value() + : 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(); + out.speed = p["speed"].get_value(); + out.rotation_delta_min = p["rotation_delta_min"].get_value(); + out.rotation_delta_max = p["rotation_delta_max"].get_value(); + 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(); + } + }; + 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(); + return true; + } + } // namespace auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type) -> std::optional { try { EnemyConfig cfg; - cfg.name = node.contains("name") ? node["name"].get_value() : "enemy"; - // ai_type — validació estricta contra el tipus esperat - if (!node.contains("ai_type")) { - std::cerr << "[EnemyConfig] Error: falta 'ai_type' a " << cfg.name << '\n'; - return std::nullopt; - } - const auto AI_STR = node["ai_type"].get_value(); - 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(); - - // 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(); - cfg.physics.speed = physics["speed"].get_value(); - cfg.physics.rotation_delta_min = physics["rotation_delta_min"].get_value(); - cfg.physics.rotation_delta_max = physics["rotation_delta_max"].get_value(); - cfg.physics.collision_radius = physics["collision_radius"].get_value(); - - // 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(); - } - if (b.contains("angle_change_max")) { - cfg.behavior.angle_change_max = b["angle_change_max"].get_value(); - } - if (b.contains("tracking_strength")) { - cfg.behavior.tracking_strength = b["tracking_strength"].get_value(); - } - if (b.contains("tracking_interval")) { - cfg.behavior.tracking_interval = b["tracking_interval"].get_value(); - } - if (b.contains("rotation_proximity_multiplier")) { - cfg.behavior.rotation_proximity_multiplier = b["rotation_proximity_multiplier"].get_value(); - } - if (b.contains("proximity_distance")) { - cfg.behavior.proximity_distance = b["proximity_distance"].get_value(); - } - } - - // 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(); + if (!parseAiType(node, expected_ai_type, cfg.name, cfg.ai_type)) { return std::nullopt; } + if (!parseShape(node, cfg.name, cfg.shape)) { return std::nullopt; } + if (!parsePhysics(node, cfg.name, cfg.physics)) { return std::nullopt; } + parseBehavior(node, cfg.behavior); + if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; } + if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; } return cfg; } catch (const std::exception& e) { diff --git a/source/game/entities/enemy_config.hpp b/source/game/entities/enemy_config.hpp index 3292981..29b748d 100644 --- a/source/game/entities/enemy_config.hpp +++ b/source/game/entities/enemy_config.hpp @@ -18,6 +18,8 @@ struct EnemyConfig { struct ShapeCfg { 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 { @@ -25,7 +27,6 @@ struct EnemyConfig { float speed; float rotation_delta_min; float rotation_delta_max; - float collision_radius; }; // Camps específics de cada AI. Els no aplicables a un tipus queden a 0.0F diff --git a/source/game/entities/player_config.cpp b/source/game/entities/player_config.cpp index e61a8dd..c816657 100644 --- a/source/game/entities/player_config.cpp +++ b/source/game/entities/player_config.cpp @@ -7,7 +7,6 @@ #include #include #include -#include namespace { @@ -41,7 +40,12 @@ auto PlayerConfig::fromYaml(const fkyaml::node& node) -> std::optional(); + const auto& shape = node["shape"]; + cfg.shape.path = shape["path"].get_value(); + cfg.shape.scale = shape.contains("scale") ? shape["scale"].get_value() : 1.0F; + cfg.shape.collision_factor = shape.contains("collision_factor") + ? shape["collision_factor"].get_value() + : 1.0F; // physics if (!node.contains("physics")) { @@ -53,7 +57,6 @@ auto PlayerConfig::fromYaml(const fkyaml::node& node) -> std::optional(); cfg.physics.linear_damping = physics["linear_damping"].get_value(); cfg.physics.angular_damping = physics["angular_damping"].get_value(); - cfg.physics.collision_radius = physics["collision_radius"].get_value(); cfg.physics.rotation_speed = physics["rotation_speed"].get_value(); cfg.physics.acceleration = physics["acceleration"].get_value(); cfg.physics.max_velocity = physics["max_velocity"].get_value(); diff --git a/source/game/entities/player_config.hpp b/source/game/entities/player_config.hpp index a7becb6..54909e1 100644 --- a/source/game/entities/player_config.hpp +++ b/source/game/entities/player_config.hpp @@ -17,6 +17,8 @@ struct PlayerConfig { struct ShapeCfg { 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 { @@ -24,7 +26,6 @@ struct PlayerConfig { float restitution; float linear_damping; float angular_damping; - float collision_radius; float rotation_speed; // rad/s float acceleration; // px/s^2 multiplicat per la massa float max_velocity; // px/s (clamp post-integració) diff --git a/source/game/entities/ship.cpp b/source/game/entities/ship.cpp index 0b72936..1c92b01 100644 --- a/source/game/entities/ship.cpp +++ b/source/game/entities/ship.cpp @@ -26,18 +26,22 @@ Ship::Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape config_(std::move(config)) { 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"). const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path; shape_ = Graphics::ShapeLoader::load(SHAPE_PATH); if (!shape_ || !shape_->isValid()) { 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) { @@ -144,10 +148,11 @@ void Ship::draw() const { 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 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. SDL_Color color = config_.colors.normal; diff --git a/source/game/entities/ship.hpp b/source/game/entities/ship.hpp index 3fe41fe..c9e4b4b 100644 --- a/source/game/entities/ship.hpp +++ b/source/game/entities/ship.hpp @@ -6,7 +6,6 @@ #include -#include "core/defaults.hpp" #include "core/entities/entity.hpp" #include "core/types.hpp" #include "game/entities/player_config.hpp" @@ -30,10 +29,9 @@ class Ship : public Entities::Entity { // Override: Interfaz de Entity [[nodiscard]] auto isActive() const -> bool override { return !is_hit_; } - // Override: Interfaz de colisión - [[nodiscard]] auto getCollisionRadius() const -> float override { - return config_.physics.collision_radius; - } + // Override: Interfaz de colisión. Derivat al ctor del bounding_radius del + // shape carregat × scale × collision_factor. + [[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; } [[nodiscard]] auto isCollidable() const -> bool override { 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. PlayerConfig config_{}; + // Radi de col·lisió derivat: shape.bounding_radius × shape.scale × shape.collision_factor. + float collision_radius_{0.0F}; + bool is_hit_{false}; float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable