// enemy_config.cpp - Implementació del parser de EnemyConfig // © 2026 JailDesigner #include "game/entities/enemy_config.hpp" #include #include #include #include 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(); const auto G = node[1].get_value(); const auto B = node[2].get_value(); out = SDL_Color{ .r = static_cast(R), .g = static_cast(G), .b = static_cast(B), .a = 255}; return true; } auto aiTypeFromString(const std::string& s) -> std::optional { if (s == "pentagon") { return EnemyType::PENTAGON; } if (s == "square") { return EnemyType::SQUARE; } if (s == "pinwheel") { return EnemyType::PINWHEEL; } return std::nullopt; } } // 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(); return cfg; } catch (const std::exception& e) { std::cerr << "[EnemyConfig] Excepció parsejant: " << e.what() << '\n'; return std::nullopt; } }