Merge branch 'feat/entities-shape-scale': collision_radius derivat del shape + scale al YAML
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<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
|
||||
|
||||
auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
|
||||
-> std::optional<EnemyConfig> {
|
||||
try {
|
||||
EnemyConfig cfg;
|
||||
|
||||
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "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<std::string>();
|
||||
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>();
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -41,7 +40,12 @@ auto PlayerConfig::fromYaml(const fkyaml::node& node) -> std::optional<PlayerCon
|
||||
std::cerr << "[PlayerConfig] Error: falta 'shape.path'" << '\n';
|
||||
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
|
||||
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.linear_damping = physics["linear_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.acceleration = physics["acceleration"].get_value<float>();
|
||||
cfg.physics.max_velocity = physics["max_velocity"].get_value<float>();
|
||||
|
||||
@@ -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ó)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user