From 6447932212f8830be82c735182a2f8b5caa7a05b Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 25 May 2026 08:32:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(entities):=20migrar=20la=20configuraci?= =?UTF-8?q?=C3=B3=20del=20player=20a=20data/entities/player/player.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/entities/player/player.yaml | 43 +++++++++ source/core/defaults.hpp | 1 - source/core/defaults/enemies.hpp | 13 +-- source/core/defaults/entities.hpp | 2 +- source/core/defaults/game.hpp | 2 +- source/core/defaults/physics.hpp | 16 +--- source/core/defaults/ship.hpp | 33 ------- source/core/defaults/trail.hpp | 2 +- source/core/entities/entity_loader.cpp | 56 ++++++++++++ source/core/entities/entity_loader.hpp | 38 ++++++++ source/game/entities/bullet.cpp | 4 +- source/game/entities/bullet.hpp | 2 +- source/game/entities/player_config.cpp | 108 +++++++++++++++++++++++ source/game/entities/player_config.hpp | 72 +++++++++++++++ source/game/entities/ship.cpp | 74 ++++++---------- source/game/entities/ship.hpp | 25 +++--- source/game/scenes/game_scene.cpp | 23 ++++- source/game/systems/collision_system.cpp | 3 +- 18 files changed, 396 insertions(+), 121 deletions(-) create mode 100644 data/entities/player/player.yaml delete mode 100644 source/core/defaults/ship.hpp create mode 100644 source/core/entities/entity_loader.cpp create mode 100644 source/core/entities/entity_loader.hpp create mode 100644 source/game/entities/player_config.cpp create mode 100644 source/game/entities/player_config.hpp diff --git a/data/entities/player/player.yaml b/data/entities/player/player.yaml new file mode 100644 index 0000000..58885c3 --- /dev/null +++ b/data/entities/player/player.yaml @@ -0,0 +1,43 @@ +name: player_ship + +# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/"). +# Nota: el segon jugador rep un override del shape ("ship2.shp") al ctor. +# Quan s'introdueixin variants reals de nau, es crearà un YAML separat +# per cada model. +shape: + path: ship.shp + +physics: + mass: 10.0 + restitution: 0.6 + linear_damping: 1.5 + angular_damping: 0.0 + collision_radius: 12.0 + rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia) + acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST + max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade) + # Factor de transferència del moment lineal de la nau a l'enemic en el + # frame exacte que mor per col·lisió (afegit per damunt del rebot natural). + death_impact_factor: 0.3 + +invulnerability: + duration: 3.0 # segons d'invulnerabilitat post-respawn + blink_visible: 0.1 # segons visible per cicle de parpelleig + blink_invisible: 0.1 # segons invisible per cicle de parpelleig + +hurt: + duration: 15.0 # segons en estat "ferit" abans de tornar a normal + blink_hz: 10.0 # freqüència parpelleig color normal <-> color hurt + +# Empenta visual: la nau s'escala lleugerament amb la velocitat. +# Manté la sensació del Pascal original (0..MAX_VEL → 1.0..~1.5). +visual_thrust: + push_divisor: 33.33 + scale_divisor: 12.0 + +colors: + normal: [255, 255, 255] # blanc neutre + hurt: [255, 220, 60] # daurat (estat ferit) + +weapon: + bullet_speed: 700.0 # velocitat escalar de la bullet (px/s) diff --git a/source/core/defaults.hpp b/source/core/defaults.hpp index c978a14..a636eaa 100644 --- a/source/core/defaults.hpp +++ b/source/core/defaults.hpp @@ -25,7 +25,6 @@ #include "core/defaults/physics.hpp" #include "core/defaults/playfield.hpp" #include "core/defaults/rendering.hpp" -#include "core/defaults/ship.hpp" #include "core/defaults/starfield_parallax.hpp" #include "core/defaults/title.hpp" #include "core/defaults/trail.hpp" diff --git a/source/core/defaults/enemies.hpp b/source/core/defaults/enemies.hpp index 0834164..5d34216 100644 --- a/source/core/defaults/enemies.hpp +++ b/source/core/defaults/enemies.hpp @@ -3,8 +3,6 @@ #pragma once -#include "core/defaults/entities.hpp" - namespace Defaults::Enemies { // Cuerpo físico común (valores por defecto del constructor) @@ -78,10 +76,13 @@ namespace Defaults::Enemies { // Spawn safety and invulnerability system namespace Spawn { - // Safe spawn distance from player - constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius - constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px - constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position + // Safe spawn distance from player. Antic: SHIP_RADIUS(12) * 3 = 36 px. + // SHIP_RADIUS ha migrat al YAML del player; aquesta constant es + // mantindrà fixa fins al PR de migració dels enemics a YAML, on + // passarà a derivar-se en runtime del player_config. + constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; + constexpr float SAFETY_DISTANCE = 36.0F; + constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position // Invulnerability system constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds diff --git a/source/core/defaults/entities.hpp b/source/core/defaults/entities.hpp index a1e2178..274dedf 100644 --- a/source/core/defaults/entities.hpp +++ b/source/core/defaults/entities.hpp @@ -8,7 +8,7 @@ namespace Defaults::Entities { constexpr int MAX_ORNIS = 15; constexpr int MAX_BULLETS = 50; - constexpr float SHIP_RADIUS = 12.0F; + // SHIP_RADIUS migrat a data/entities/player/player.yaml (physics.collision_radius). constexpr float ENEMY_RADIUS = 20.0F; constexpr float BULLET_RADIUS = 3.0F; diff --git a/source/core/defaults/game.hpp b/source/core/defaults/game.hpp index a7299c2..55ed3dc 100644 --- a/source/core/defaults/game.hpp +++ b/source/core/defaults/game.hpp @@ -29,7 +29,7 @@ namespace Defaults::Game { // Friendly fire system constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%) - constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS + // BULLET_SPEED migrat a data/entities/player/player.yaml (weapon.bullet_speed). // Transición LEVEL_START (mensajes aleatorios PRE-level) constexpr float LEVEL_START_DURATION = 3.0F; // Duración total diff --git a/source/core/defaults/physics.hpp b/source/core/defaults/physics.hpp index 3fa8f7a..2f3f741 100644 --- a/source/core/defaults/physics.hpp +++ b/source/core/defaults/physics.hpp @@ -5,10 +5,10 @@ namespace Defaults::Physics { - constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s) - constexpr float ACCELERATION = 400.0F; // px/s² - constexpr float MAX_VELOCITY = 180.0F; // px/s - constexpr float FRICTION = 20.0F; // px/s² + // NOTA: els paràmetres específics de la nau del player (rotation_speed, + // acceleration, max_velocity, death_impact_factor) viuen ara a + // data/entities/player/player.yaml. La migració d'aquests fitxers va + // començar amb la nau; els enemics i les bales són els següents. // Bullet — impacto físico contra enemigo (impulse mass-aware). // Model: el impulse és el moment lineal de la bala (m·v) multiplicat per @@ -18,14 +18,6 @@ namespace Defaults::Physics { constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic } // namespace Bullet - // Ship → enemy: impuls explícit aplicat a l'enemic en el moment exacte - // que la nau mor per col·lisió amb ell (afegit per damunt del rebot - // natural de PhysicsWorld, que ja és present però subtil amb la - // damping de la nau). - namespace Ship { - constexpr float DEATH_IMPACT_MOMENTUM_FACTOR = 0.3F; - } // namespace Ship - // Explosions (debris physics) namespace Debris { constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s) diff --git a/source/core/defaults/ship.hpp b/source/core/defaults/ship.hpp deleted file mode 100644 index 5ad8e89..0000000 --- a/source/core/defaults/ship.hpp +++ /dev/null @@ -1,33 +0,0 @@ -// ship.hpp - Configuració de la nau (invulnerabilitat, parpelleig) -// © 2026 JailDesigner - -#pragma once - -namespace Defaults::Ship { - - // Invulnerabilidad post-respawn - constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad - - // Parpadeo visual durante invulnerabilidad - constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos) - constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos) - // Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s) - - // Cuerpo físico - constexpr float MASS = 10.0F; // Masa de referencia para choques - constexpr float RESTITUTION = 0.6F; // Rebote moderado contra paredes - constexpr float LINEAR_DAMPING = 1.5F; // Fricción exponencial (s⁻¹) - constexpr float ANGULAR_DAMPING = 0.0F; // Rotación 100% por input (no inercial) - - // Empuje visual: escala proporcional a la velocidad (0..200 px/s → 1.0..1.5) - // Mantiene la sensación del Pascal original. - constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual - constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR) - - // Estat "ferit": entre primera col·lisió amb enemic i recuperació o segona col·lisió mortal. - namespace Hurt { - constexpr float DURATION = 15.0F; // Segons en estat ferit (provisional) - constexpr float BLINK_HZ = 10.0F; // Freqüència parpelleig color normal ↔ ferit - } // namespace Hurt - -} // namespace Defaults::Ship diff --git a/source/core/defaults/trail.hpp b/source/core/defaults/trail.hpp index d1d406c..81659d1 100644 --- a/source/core/defaults/trail.hpp +++ b/source/core/defaults/trail.hpp @@ -7,7 +7,7 @@ namespace Defaults::Trail { constexpr int POOL_SIZE = 200; - constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de Physics::MAX_VELOCITY (180) + constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de player.yaml::physics.max_velocity (180 px/s) constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement diff --git a/source/core/entities/entity_loader.cpp b/source/core/entities/entity_loader.cpp new file mode 100644 index 0000000..9703d52 --- /dev/null +++ b/source/core/entities/entity_loader.cpp @@ -0,0 +1,56 @@ +// entity_loader.cpp - Implementació del carregador d'entitats YAML +// © 2026 JailDesigner + +#include "core/entities/entity_loader.hpp" + +#include +#include +#include +#include +#include +#include + +#include "core/resources/resource_helper.hpp" + +namespace Entities { + + std::unordered_map> EntityLoader::cache; + + auto EntityLoader::load(const std::string& name) -> std::shared_ptr { + // Cache hit + auto it = cache.find(name); + if (it != cache.end()) { + std::cout << "[EntityLoader] Cache hit: " << name << '\n'; + return it->second; + } + + const std::string PATH = "entities/" + name + "/" + name + ".yaml"; + + std::vector data = Resource::Helper::loadFile(PATH); + if (data.empty()) { + std::cerr << "[EntityLoader] Error: no s'ha pogut load " << PATH << '\n'; + return nullptr; + } + + try { + std::string yaml_content(data.begin(), data.end()); + std::stringstream stream(yaml_content); + auto node = std::make_shared(fkyaml::node::deserialize(stream)); + + std::cout << "[EntityLoader] Carregat: " << PATH << '\n'; + cache[name] = node; + return node; + } catch (const std::exception& e) { + std::cerr << "[EntityLoader] Excepció parsejant " << PATH << ": " << e.what() << '\n'; + return nullptr; + } + } + + void EntityLoader::clearCache() { + std::cout << "[EntityLoader] Netejant caché (" << cache.size() << " entitats)" << '\n'; + cache.clear(); + } + + auto EntityLoader::getCacheSize() -> size_t { return cache.size(); } + +} // namespace Entities diff --git a/source/core/entities/entity_loader.hpp b/source/core/entities/entity_loader.hpp new file mode 100644 index 0000000..a89511d --- /dev/null +++ b/source/core/entities/entity_loader.hpp @@ -0,0 +1,38 @@ +// entity_loader.hpp - Carregador genèric de descriptors d'entitats en YAML +// © 2026 JailDesigner +// +// Cada entitat viu a `data/entities//.yaml` (mateix patró que el +// projecte germà aee_arcade). Aquest loader resol el path, llegeix del +// resource pack via Resource::Helper, parseja amb fkyaml i cacheja el node +// per evitar relectures. Retorna nullptr en cas d'error (el caller decideix +// si abortar). + +#pragma once + +#include +#include +#include + +#include "external/fkyaml_node.hpp" + +namespace Entities { + + class EntityLoader { + public: + EntityLoader() = delete; // tot estàtic + + // Carrega el descriptor d'una entitat per nom (ex. "player" → + // "entities/player/player.yaml"). Retorna nullptr si no es pot + // carregar o parsejar. Cachejat per nom. + static auto load(const std::string& name) -> std::shared_ptr; + + // Buidar caché (útil per debug/recàrrega). + static void clearCache(); + + [[nodiscard]] static auto getCacheSize() -> size_t; + + private: + static std::unordered_map> cache; + }; + +} // namespace Entities diff --git a/source/game/entities/bullet.cpp b/source/game/entities/bullet.cpp index 99187d3..f5cdd41 100644 --- a/source/game/entities/bullet.cpp +++ b/source/game/entities/bullet.cpp @@ -53,7 +53,7 @@ void Bullet::init() { body_.clearAccumulators(); } -void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id) { +void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed) { // Activar bullet is_active_ = true; @@ -71,7 +71,7 @@ void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id) { body_.angle = angle; const float DIR_X = std::cos(angle - (Constants::PI / 2.0F)); const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F)); - body_.velocity = Vec2{.x = DIR_X * Defaults::Game::BULLET_SPEED, .y = DIR_Y * Defaults::Game::BULLET_SPEED}; + body_.velocity = Vec2{.x = DIR_X * bullet_speed, .y = DIR_Y * bullet_speed}; body_.angular_velocity = 0.0F; body_.clearAccumulators(); diff --git a/source/game/entities/bullet.hpp b/source/game/entities/bullet.hpp index 72c1a81..b520aab 100644 --- a/source/game/entities/bullet.hpp +++ b/source/game/entities/bullet.hpp @@ -17,7 +17,7 @@ class Bullet : public Entities::Entity { explicit Bullet(Rendering::Renderer* renderer); void init() override; - void fire(const Vec2& position, float angle, uint8_t owner_id); + void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed); void update(float delta_time) override; void postUpdate(float delta_time) override; void draw() const override; diff --git a/source/game/entities/player_config.cpp b/source/game/entities/player_config.cpp new file mode 100644 index 0000000..e61a8dd --- /dev/null +++ b/source/game/entities/player_config.cpp @@ -0,0 +1,108 @@ +// player_config.cpp - Implementació del parser de PlayerConfig +// © 2026 JailDesigner + +#include "game/entities/player_config.hpp" + +#include +#include +#include +#include +#include + +namespace { + + // Helper: extreu un SDL_Color d'una seqüència de 3 enters [r, g, b] del YAML. + // Retorna true si el format és vàlid. + 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; + } + +} // namespace + +auto PlayerConfig::fromYaml(const fkyaml::node& node) -> std::optional { + try { + PlayerConfig cfg; + + cfg.name = node.contains("name") ? node["name"].get_value() : "player"; + + // shape + if (!node.contains("shape") || !node["shape"].contains("path")) { + std::cerr << "[PlayerConfig] Error: falta 'shape.path'" << '\n'; + return std::nullopt; + } + cfg.shape.path = node["shape"]["path"].get_value(); + + // physics + if (!node.contains("physics")) { + std::cerr << "[PlayerConfig] Error: falta 'physics'" << '\n'; + return std::nullopt; + } + const auto& physics = node["physics"]; + cfg.physics.mass = physics["mass"].get_value(); + cfg.physics.restitution = physics["restitution"].get_value(); + cfg.physics.linear_damping = physics["linear_damping"].get_value(); + cfg.physics.angular_damping = physics["angular_damping"].get_value(); + cfg.physics.collision_radius = physics["collision_radius"].get_value(); + cfg.physics.rotation_speed = physics["rotation_speed"].get_value(); + cfg.physics.acceleration = physics["acceleration"].get_value(); + cfg.physics.max_velocity = physics["max_velocity"].get_value(); + cfg.physics.death_impact_factor = physics["death_impact_factor"].get_value(); + + // invulnerability + if (!node.contains("invulnerability")) { + std::cerr << "[PlayerConfig] Error: falta 'invulnerability'" << '\n'; + return std::nullopt; + } + const auto& invul = node["invulnerability"]; + cfg.invulnerability.duration = invul["duration"].get_value(); + cfg.invulnerability.blink_visible = invul["blink_visible"].get_value(); + cfg.invulnerability.blink_invisible = invul["blink_invisible"].get_value(); + + // hurt + if (!node.contains("hurt")) { + std::cerr << "[PlayerConfig] Error: falta 'hurt'" << '\n'; + return std::nullopt; + } + cfg.hurt.duration = node["hurt"]["duration"].get_value(); + cfg.hurt.blink_hz = node["hurt"]["blink_hz"].get_value(); + + // visual_thrust + if (!node.contains("visual_thrust")) { + std::cerr << "[PlayerConfig] Error: falta 'visual_thrust'" << '\n'; + return std::nullopt; + } + cfg.visual_thrust.push_divisor = node["visual_thrust"]["push_divisor"].get_value(); + cfg.visual_thrust.scale_divisor = node["visual_thrust"]["scale_divisor"].get_value(); + + // colors + if (!node.contains("colors") || + !parseColor(node["colors"]["normal"], cfg.colors.normal) || + !parseColor(node["colors"]["hurt"], cfg.colors.hurt)) { + std::cerr << "[PlayerConfig] Error: 'colors.normal' / 'colors.hurt' no són seqüències [r,g,b]" << '\n'; + return std::nullopt; + } + + // weapon + if (!node.contains("weapon")) { + std::cerr << "[PlayerConfig] Error: falta 'weapon'" << '\n'; + return std::nullopt; + } + cfg.weapon.bullet_speed = node["weapon"]["bullet_speed"].get_value(); + + return cfg; + } catch (const std::exception& e) { + std::cerr << "[PlayerConfig] Excepció parsejant: " << e.what() << '\n'; + return std::nullopt; + } +} diff --git a/source/game/entities/player_config.hpp b/source/game/entities/player_config.hpp new file mode 100644 index 0000000..a7becb6 --- /dev/null +++ b/source/game/entities/player_config.hpp @@ -0,0 +1,72 @@ +// player_config.hpp - Configuració de la nau del player carregada des de YAML +// © 2026 JailDesigner +// +// POD struct amb sub-structs per organitzar els paràmetres del jugador +// (física, invulnerabilitat, hurt, empenta visual, colors, weapon). Es +// construeix a partir d'un node fkyaml carregat per EntityLoader. + +#pragma once + +#include + +#include +#include + +#include "external/fkyaml_node.hpp" + +struct PlayerConfig { + struct ShapeCfg { + std::string path; + }; + + struct PhysicsCfg { + float mass; + float restitution; + float linear_damping; + float angular_damping; + float collision_radius; + float rotation_speed; // rad/s + float acceleration; // px/s^2 multiplicat per la massa + float max_velocity; // px/s (clamp post-integració) + float death_impact_factor; // [0..1] moment transferit a l'enemic al morir + }; + + struct InvulnerabilityCfg { + float duration; + float blink_visible; + float blink_invisible; + }; + + struct HurtCfg { + float duration; + float blink_hz; + }; + + struct VisualThrustCfg { + float push_divisor; + float scale_divisor; + }; + + struct ColorsCfg { + SDL_Color normal; + SDL_Color hurt; + }; + + struct WeaponCfg { + float bullet_speed; + }; + + std::string name; + ShapeCfg shape; + PhysicsCfg physics; + InvulnerabilityCfg invulnerability; + HurtCfg hurt; + VisualThrustCfg visual_thrust; + ColorsCfg colors; + WeaponCfg weapon; + + // Construeix un PlayerConfig a partir del node YAML. Retorna std::nullopt + // si falten camps requerits o el format no és vàlid (el caller decideix + // si abortar). + static auto fromYaml(const fkyaml::node& node) -> std::optional; +}; diff --git a/source/game/entities/ship.cpp b/source/game/entities/ship.cpp index 4313b01..0b72936 100644 --- a/source/game/entities/ship.cpp +++ b/source/game/entities/ship.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "core/audio/audio.hpp" #include "core/defaults.hpp" @@ -20,27 +21,26 @@ #include "core/types.hpp" #include "game/constants.hpp" -Ship::Ship(Rendering::Renderer* renderer, const char* shape_file) - : Entity(renderer) { - // Brightness específico para naves +Ship::Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape_override) + : Entity(renderer), + config_(std::move(config)) { brightness_ = Defaults::Brightness::NAU; - // Configuración del cuerpo físico - body_.setMass(Defaults::Ship::MASS); - body_.radius = Defaults::Entities::SHIP_RADIUS; - body_.restitution = Defaults::Ship::RESTITUTION; - body_.linear_damping = Defaults::Ship::LINEAR_DAMPING; - body_.angular_damping = Defaults::Ship::ANGULAR_DAMPING; + body_.setMass(config_.physics.mass); + body_.radius = config_.physics.collision_radius; + body_.restitution = config_.physics.restitution; + body_.linear_damping = config_.physics.linear_damping; + body_.angular_damping = config_.physics.angular_damping; - // Cargar shape compartida desde archivo - shape_ = Graphics::ShapeLoader::load(shape_file); + // El shape pot venir del YAML o ser overridden (ex: P2 amb "ship2.shp"). + const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path; + shape_ = Graphics::ShapeLoader::load(SHAPE_PATH); if (!shape_ || !shape_->isValid()) { - std::cerr << "[Ship] Error: no se ha podido cargar " << shape_file << '\n'; + std::cerr << "[Ship] Error: no se ha podido cargar " << SHAPE_PATH << '\n'; } } void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) { - // Posición inicial if (spawn_point != nullptr) { center_ = *spawn_point; } else { @@ -50,34 +50,27 @@ void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) { center_ = {.x = center_x, .y = center_y}; } - // Reset orientación angle_ = 0.0F; - // Sincronizar cuerpo físico con la posición/orientación inicial body_.position = center_; body_.angle = angle_; body_.velocity = Vec2{}; body_.angular_velocity = 0.0F; body_.clearAccumulators(); - // Activar invulnerabilidad solo si es respawn - invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F; + invulnerable_timer_ = activar_invulnerabilitat ? config_.invulnerability.duration : 0.0F; is_hit_ = false; hurt_timer_ = 0.0F; touching_enemy_prev_frame_ = false; } void Ship::processInput(float delta_time, uint8_t player_id) { - // Solo procesa input si la nave está viva if (is_hit_) { return; } auto* input = Input::get(); - // Rotación: control directo del ángulo (no física, no inercial). - // Se actualiza también body_.angle para que el dibujado tras - // postUpdate refleje el cambio inmediatamente. const bool ROTATE_RIGHT = (player_id == 0) ? input->checkActionPlayer1(InputAction::RIGHT, Input::ALLOW_REPEAT) : input->checkActionPlayer2(InputAction::RIGHT, Input::ALLOW_REPEAT); @@ -89,10 +82,10 @@ void Ship::processInput(float delta_time, uint8_t player_id) { : input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT); if (ROTATE_RIGHT) { - body_.angle += Defaults::Physics::ROTATION_SPEED * delta_time; + body_.angle += config_.physics.rotation_speed * delta_time; } if (ROTATE_LEFT) { - body_.angle -= Defaults::Physics::ROTATION_SPEED * delta_time; + body_.angle -= config_.physics.rotation_speed * delta_time; } // Thrust: fuerza vectorial en la dirección de la nariz. @@ -100,44 +93,36 @@ void Ship::processInput(float delta_time, uint8_t player_id) { if (THRUST) { const float DIR_X = std::cos(body_.angle - (Constants::PI / 2.0F)); const float DIR_Y = std::sin(body_.angle - (Constants::PI / 2.0F)); - // Fuerza = masa * aceleración: 10 kg * 400 px/s² = 4000 (unidades arcade) - const float MAGNITUDE = body_.mass * Defaults::Physics::ACCELERATION; + const float MAGNITUDE = body_.mass * config_.physics.acceleration; body_.applyForce(Vec2{.x = DIR_X * MAGNITUDE, .y = DIR_Y * MAGNITUDE}); } } void Ship::update(float delta_time) { - // Solo update si la nave está viva if (is_hit_) { return; } - // Decrementar timer de invulnerabilidad if (invulnerable_timer_ > 0.0F) { invulnerable_timer_ -= delta_time; invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F); } - // Decrementar timer d'estat HURT (a 0 → torna a normal sense efecte extern) if (hurt_timer_ > 0.0F) { hurt_timer_ -= delta_time; hurt_timer_ = std::max(hurt_timer_, 0.0F); } - // El movimiento real lo hace PhysicsWorld::update(). - // Aquí solo lógica de estado. - // Cap de velocidad: el thrust acumula fuerza sin límite; limitamos // la magnitud de body_.velocity tras aplicar fuerzas para preservar // el feel arcade del MAX_VELOCITY original. const float CURRENT_SPEED = body_.velocity.length(); - if (CURRENT_SPEED > Defaults::Physics::MAX_VELOCITY) { - body_.velocity = body_.velocity * (Defaults::Physics::MAX_VELOCITY / CURRENT_SPEED); + if (CURRENT_SPEED > config_.physics.max_velocity) { + body_.velocity = body_.velocity * (config_.physics.max_velocity / CURRENT_SPEED); } } void Ship::postUpdate(float /*delta_time*/) { - // Sincronizar mirror desde body_ tras la integración del world. center_ = body_.position; angle_ = body_.angle; } @@ -147,11 +132,10 @@ void Ship::draw() const { return; } - // Parpadeo si invulnerable if (isInvulnerable()) { - const float BLINK_CYCLE = Defaults::Ship::BLINK_VISIBLE_TIME + Defaults::Ship::BLINK_INVISIBLE_TIME; + const float BLINK_CYCLE = config_.invulnerability.blink_visible + config_.invulnerability.blink_invisible; const float TIME_IN_CYCLE = std::fmod(invulnerable_timer_, BLINK_CYCLE); - if (TIME_IN_CYCLE < Defaults::Ship::BLINK_INVISIBLE_TIME) { + if (TIME_IN_CYCLE < config_.invulnerability.blink_invisible) { return; } } @@ -161,19 +145,17 @@ void Ship::draw() const { } // Efecto visual de empuje: escala proporcional a la velocidad. - // 0..200 px/s → escala 1.0..1.5 (manteniendo la sensación del Pascal original). const float SPEED = getSpeed(); - const float VISUAL_PUSH = SPEED / Defaults::Ship::VISUAL_PUSH_DIVISOR; - const float SCALE = 1.0F + (VISUAL_PUSH / Defaults::Ship::VISUAL_SCALE_DIVISOR); + const float VISUAL_PUSH = SPEED / config_.visual_thrust.push_divisor; + const float SCALE = 1.0F + (VISUAL_PUSH / config_.visual_thrust.scale_divisor); - // Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt - // a Hurt::BLINK_HZ (mateixa estètica que el wounded dels enemics). - SDL_Color color = color_normal_; + // Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt. + SDL_Color color = config_.colors.normal; if (hurt_timer_ > 0.0F) { - const float CYCLE = 1.0F / Defaults::Ship::Hurt::BLINK_HZ; + const float CYCLE = 1.0F / config_.hurt.blink_hz; const float T = std::fmod(hurt_timer_, CYCLE); if (T < (CYCLE / 2.0F)) { - color = color_hurt_; + color = config_.colors.hurt; } } @@ -181,6 +163,6 @@ void Ship::draw() const { } void Ship::hurt() { - hurt_timer_ = Defaults::Ship::Hurt::DURATION; + hurt_timer_ = config_.hurt.duration; Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME); } diff --git a/source/game/entities/ship.hpp b/source/game/entities/ship.hpp index a5b64cc..3fe41fe 100644 --- a/source/game/entities/ship.hpp +++ b/source/game/entities/ship.hpp @@ -9,12 +9,16 @@ #include "core/defaults.hpp" #include "core/entities/entity.hpp" #include "core/types.hpp" +#include "game/entities/player_config.hpp" class Ship : public Entities::Entity { public: Ship() : Entity(nullptr) {} - explicit Ship(Rendering::Renderer* renderer, const char* shape_file = "ship.shp"); + // shape_override: si no és nullptr, substitueix config.shape.path + // (utilitzat per donar al P2 un model visual diferent compartint la + // mateixa configuració del player). + explicit Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape_override = nullptr); void init() override { init(nullptr, false); } void init(const Vec2* spawn_point, bool activar_invulnerabilitat = false); @@ -28,7 +32,7 @@ class Ship : public Entities::Entity { // Override: Interfaz de colisión [[nodiscard]] auto getCollisionRadius() const -> float override { - return Defaults::Entities::SHIP_RADIUS; + return config_.physics.collision_radius; } [[nodiscard]] auto isCollidable() const -> bool override { return !is_hit_ && invulnerable_timer_ <= 0.0F; @@ -36,10 +40,9 @@ class Ship : public Entities::Entity { // Getters [[nodiscard]] auto isInvulnerable() const -> bool { return invulnerable_timer_ > 0.0F; } - // Velocidad como vector cartesiano (ahora viene directa del body_). [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } - // Velocidad escalar (utilidad para draw y debugging). [[nodiscard]] auto getSpeed() const -> float { return body_.velocity.length(); } + [[nodiscard]] auto getConfig() const -> const PlayerConfig& { return config_; } // Setters void setCenter(const Vec2& nou_centre) { @@ -65,17 +68,15 @@ class Ship : public Entities::Entity { void setTouchingEnemyPrevFrame(bool touching) { touching_enemy_prev_frame_ = touching; } private: - // Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_). - // Inicializados en la declaración: el ctor por defecto deja la nave "viva y sin invulnerabilidad", - // que es el estado coherente al que llevan tanto init() como el ctor con renderer. + // Configuració carregada des de YAML. Default-init zero permet el ctor + // per defecte (necessari per a `std::array`); s'omple via + // copy/move-assignment quan GameScene crea la nau real. + PlayerConfig config_{}; + bool is_hit_{false}; float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable - // Colors de la nau (propietats, prep per migració a YAML). - SDL_Color color_normal_{Defaults::Palette::SHIP}; - SDL_Color color_hurt_{Defaults::Palette::WOUNDED}; - - // >0 → estat HURT (parpelleig color_normal_ ↔ color_hurt_). + // >0 → estat HURT (parpelleig color normal ↔ color hurt). float hurt_timer_{0.0F}; // Edge-trigger: true si el frame anterior la nau ja estava en contacte amb un enemic. diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 798c5b2..4333e87 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -10,10 +10,12 @@ #include #include "core/audio/audio.hpp" +#include "core/entities/entity_loader.hpp" #include "core/input/input.hpp" #include "core/locale/locale.hpp" #include "core/system/scene_context.hpp" #include "core/system/service_menu.hpp" +#include "game/entities/player_config.hpp" #include "game/stage_system/stage_loader.hpp" #include "game/systems/collision_system.hpp" #include "game/systems/continue_system.hpp" @@ -49,9 +51,22 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) auto option = context_.consumeOption(); (void)option; // Suprimir warning de variable no usada - // Inicialitzar naves con renderer (P1=ship.shp, P2=ship2.shp) - ships_[0] = Ship(sdl.getRenderer(), "ship.shp"); // Jugador 1: nave estàndar - ships_[1] = Ship(sdl.getRenderer(), "ship2.shp"); // Jugador 2: interceptor con ales + // Carregar la configuració del player des de YAML. Sense fallback: si + // falla, abortem (la nau no és construïble sense paràmetres). + auto player_yaml = Entities::EntityLoader::load("player"); + if (!player_yaml) { + std::cerr << "[GameScene] FATAL: no s'ha pogut carregar data/entities/player/player.yaml\n"; + std::exit(EXIT_FAILURE); + } + auto player_config = PlayerConfig::fromYaml(*player_yaml); + if (!player_config) { + std::cerr << "[GameScene] FATAL: player.yaml mal format\n"; + std::exit(EXIT_FAILURE); + } + + // Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual. + ships_[0] = Ship(sdl.getRenderer(), *player_config); // Jugador 1: nau estàndard + ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship2.shp"); // Jugador 2: interceptor amb ales // Inicialitzar balas con renderer std::ranges::fill(bullets_, Bullet(sdl.getRenderer())); @@ -944,7 +959,7 @@ void GameScene::fireBullet(uint8_t player_id) { const int START_IDX = player_id * SLOTS_PER_PLAYER; for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) { if (!bullets_[i].isActive()) { - bullets_[i].fire(fire_position, ship_angle, player_id); + bullets_[i].fire(fire_position, ship_angle, player_id, ships_[player_id].getConfig().weapon.bullet_speed); break; } } diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index 3a6bbac..885a8a3 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -224,7 +224,8 @@ namespace Systems::Collision { // Segon impacte durant HURT → mort. Aplica un impuls afegit // perquè l'enemic surti disparat (feedback visible). const Vec2 SHIP_VEL = ctx.ships[i].getVelocityVector(); - const Vec2 IMPULSE = SHIP_VEL * (Defaults::Ship::MASS * Defaults::Physics::Ship::DEATH_IMPACT_MOMENTUM_FACTOR); + const float DEATH_FACTOR = ctx.ships[i].getConfig().physics.death_impact_factor; + const Vec2 IMPULSE = SHIP_VEL * (ctx.ships[i].getBody().mass * DEATH_FACTOR); touched_enemy->applyImpulse(IMPULSE); ctx.on_player_hit(i); } else {