// 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; } if (s == "star") { return EnemyType::STAR; } 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(); out.restitution = p["restitution"].get_value(); out.linear_damping = p["linear_damping"].get_value(); out.angular_damping = p["angular_damping"].get_value(); return true; } auto parseAnimation(const fkyaml::node& node, const std::string& name, EnemyConfig::AnimationCfg& out) -> bool { if (!node.contains("animation") || !node["animation"].contains("pulse") || !node["animation"].contains("rotation_accel")) { std::cerr << "[EnemyConfig] Error: falta 'animation.pulse' o 'animation.rotation_accel' a " << name << '\n'; return false; } const auto& p = node["animation"]["pulse"]; out.pulse.trigger_prob_per_second = p["trigger_prob_per_second"].get_value(); out.pulse.duration_min = p["duration_min"].get_value(); out.pulse.duration_max = p["duration_max"].get_value(); out.pulse.amplitude_min = p["amplitude_min"].get_value(); out.pulse.amplitude_max = p["amplitude_max"].get_value(); out.pulse.frequency_min = p["frequency_min"].get_value(); out.pulse.frequency_max = p["frequency_max"].get_value(); const auto& r = node["animation"]["rotation_accel"]; out.rotation_accel.trigger_prob_per_second = r["trigger_prob_per_second"].get_value(); out.rotation_accel.duration_min = r["duration_min"].get_value(); out.rotation_accel.duration_max = r["duration_max"].get_value(); out.rotation_accel.multiplier_min = r["multiplier_min"].get_value(); out.rotation_accel.multiplier_max = r["multiplier_max"].get_value(); return true; } auto parseWounded(const fkyaml::node& node, const std::string& name, EnemyConfig::WoundedCfg& out) -> bool { if (!node.contains("wounded")) { std::cerr << "[EnemyConfig] Error: falta 'wounded' a " << name << '\n'; return false; } const auto& w = node["wounded"]; out.duration = w["duration"].get_value(); out.blink_hz = w["blink_hz"].get_value(); return true; } auto parseSpawn(const fkyaml::node& node, const std::string& name, EnemyConfig::SpawnCfg& out) -> bool { if (!node.contains("spawn")) { std::cerr << "[EnemyConfig] Error: falta 'spawn' a " << name << '\n'; return false; } const auto& s = node["spawn"]; out.invulnerability_duration = s["invulnerability_duration"].get_value(); out.invulnerability_brightness_start = s["invulnerability_brightness_start"].get_value(); out.invulnerability_brightness_end = s["invulnerability_brightness_end"].get_value(); out.invulnerability_scale_start = s["invulnerability_scale_start"].get_value(); out.invulnerability_scale_end = s["invulnerability_scale_end"].get_value(); out.safety_distance = s["safety_distance"].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"; 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 (!parseAnimation(node, cfg.name, cfg.animation)) { return std::nullopt; } if (!parseWounded(node, cfg.name, cfg.wounded)) { return std::nullopt; } if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; } 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; } }