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
+96 -75
View File
@@ -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) {