Files
orni-attack/source/game/entities/enemy_config.cpp
T

278 lines
12 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; }
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<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>();
out.restitution = p["restitution"].get_value<float>();
out.linear_damping = p["linear_damping"].get_value<float>();
out.angular_damping = p["angular_damping"].get_value<float>();
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<float>();
out.pulse.duration_min = p["duration_min"].get_value<float>();
out.pulse.duration_max = p["duration_max"].get_value<float>();
out.pulse.amplitude_min = p["amplitude_min"].get_value<float>();
out.pulse.amplitude_max = p["amplitude_max"].get_value<float>();
out.pulse.frequency_min = p["frequency_min"].get_value<float>();
out.pulse.frequency_max = p["frequency_max"].get_value<float>();
const auto& r = node["animation"]["rotation_accel"];
out.rotation_accel.trigger_prob_per_second = r["trigger_prob_per_second"].get_value<float>();
out.rotation_accel.duration_min = r["duration_min"].get_value<float>();
out.rotation_accel.duration_max = r["duration_max"].get_value<float>();
out.rotation_accel.multiplier_min = r["multiplier_min"].get_value<float>();
out.rotation_accel.multiplier_max = r["multiplier_max"].get_value<float>();
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<float>();
out.blink_hz = w["blink_hz"].get_value<float>();
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<float>();
out.invulnerability_brightness_start = s["invulnerability_brightness_start"].get_value<float>();
out.invulnerability_brightness_end = s["invulnerability_brightness_end"].get_value<float>();
out.invulnerability_scale_start = s["invulnerability_scale_start"].get_value<float>();
out.invulnerability_scale_end = s["invulnerability_scale_end"].get_value<float>();
out.safety_distance = s["safety_distance"].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;
}
auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> {
if (s == "set_hurt") { return EnemyActionType::SET_HURT; }
if (s == "destroy") { return EnemyActionType::DESTROY; }
if (s == "add_score") { return EnemyActionType::ADD_SCORE; }
if (s == "create_debris") { return EnemyActionType::CREATE_DEBRIS; }
if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; }
if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; }
return std::nullopt;
}
auto parseActionList(const fkyaml::node& list_node, const std::string& enemy_name, const char* event_name, std::vector<EnemyAction>& out) -> bool {
if (!list_node.is_sequence()) {
std::cerr << "[EnemyConfig] Error: '" << event_name << "' ha de ser una llista a "
<< enemy_name << '\n';
return false;
}
for (const auto& item : list_node) {
if (!item.contains("action")) {
std::cerr << "[EnemyConfig] Error: entrada sense 'action' a " << event_name
<< " (" << enemy_name << ")\n";
return false;
}
const auto STR = item["action"].get_value<std::string>();
const auto PARSED = actionTypeFromString(STR);
if (!PARSED) {
std::cerr << "[EnemyConfig] Error: acció desconeguda '" << STR << "' a "
<< event_name << " (" << enemy_name << ")\n";
return false;
}
out.push_back({*PARSED});
}
return true;
}
// Defaults: replica el flux hardcoded actual (set_hurt → destroy → score+debris+fireworks).
void fillLegacyDefaults(EnemyEventConfig& events) {
events.on_hit = {{EnemyActionType::SET_HURT}};
events.on_hurt_end = {{EnemyActionType::DESTROY}};
events.on_destroy = {
{EnemyActionType::ADD_SCORE},
{EnemyActionType::CREATE_DEBRIS},
{EnemyActionType::CREATE_FIREWORKS},
};
}
auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
if (!node.contains("events")) {
fillLegacyDefaults(out);
return true;
}
const auto& e = node["events"];
if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) {
return false;
}
if (e.contains("on_hurt_end") &&
!parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) {
return false;
}
if (e.contains("on_destroy") &&
!parseActionList(e["on_destroy"], name, "on_destroy", out.on_destroy)) {
return false;
}
// Validació: destroy no pot aparèixer dins on_destroy (recursió infinita).
for (const auto& a : out.on_destroy) {
if (a.type == EnemyActionType::DESTROY) {
std::cerr << "[EnemyConfig] Error: 'destroy' no pot aparèixer dins 'on_destroy' a "
<< name << " (recursió infinita)\n";
return false;
}
}
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 (!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; }
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
return cfg;
} catch (const std::exception& e) {
std::cerr << "[EnemyConfig] Excepció parsejant: " << e.what() << '\n';
return std::nullopt;
}
}