146 lines
5.8 KiB
C++
146 lines
5.8 KiB
C++
// enemy_config.cpp - Implementació del parser de EnemyConfig
|
|
// © 2026 JailDesigner
|
|
|
|
#include "game/entities/enemy_config.hpp"
|
|
|
|
#include <cstdint>
|
|
#include <exception>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
auto parseColor(const fkyaml::node& node, SDL_Color& out) -> bool {
|
|
if (!node.is_sequence() || node.size() != 3) {
|
|
return false;
|
|
}
|
|
const auto R = node[0].get_value<uint32_t>();
|
|
const auto G = node[1].get_value<uint32_t>();
|
|
const auto B = node[2].get_value<uint32_t>();
|
|
out = SDL_Color{
|
|
.r = static_cast<uint8_t>(R),
|
|
.g = static_cast<uint8_t>(G),
|
|
.b = static_cast<uint8_t>(B),
|
|
.a = 255};
|
|
return true;
|
|
}
|
|
|
|
auto aiTypeFromString(const std::string& s) -> std::optional<EnemyType> {
|
|
if (s == "pentagon") { return EnemyType::PENTAGON; }
|
|
if (s == "square") { return EnemyType::SQUARE; }
|
|
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
|
|
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";
|
|
|
|
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) {
|
|
std::cerr << "[EnemyConfig] Excepció parsejant: " << e.what() << '\n';
|
|
return std::nullopt;
|
|
}
|
|
}
|