feat(entities): migrar la configuració del player a data/entities/player/player.yaml

This commit is contained in:
JailDesigner committed 2026-05-25 08:32:49 +02:00
1 parent 9f278772bb
commit 6447932212
18 files changed
+396 -121

No files matched your search

+2 -2
View File
@@ -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();
+1 -1
View File
@@ -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;
+108
View File
@@ -0,0 +1,108 @@
// player_config.cpp - Implementació del parser de PlayerConfig
// © 2026 JailDesigner
#include "game/entities/player_config.hpp"
#include <cstdint>
#include <exception>
#include <iostream>
#include <string>
#include <vector>
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<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;
}
} // namespace
auto PlayerConfig::fromYaml(const fkyaml::node& node) -> std::optional<PlayerConfig> {
try {
PlayerConfig cfg;
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "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<std::string>();
// 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<float>();
cfg.physics.restitution = physics["restitution"].get_value<float>();
cfg.physics.linear_damping = physics["linear_damping"].get_value<float>();
cfg.physics.angular_damping = physics["angular_damping"].get_value<float>();
cfg.physics.collision_radius = physics["collision_radius"].get_value<float>();
cfg.physics.rotation_speed = physics["rotation_speed"].get_value<float>();
cfg.physics.acceleration = physics["acceleration"].get_value<float>();
cfg.physics.max_velocity = physics["max_velocity"].get_value<float>();
cfg.physics.death_impact_factor = physics["death_impact_factor"].get_value<float>();
// 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<float>();
cfg.invulnerability.blink_visible = invul["blink_visible"].get_value<float>();
cfg.invulnerability.blink_invisible = invul["blink_invisible"].get_value<float>();
// hurt
if (!node.contains("hurt")) {
std::cerr << "[PlayerConfig] Error: falta 'hurt'" << '\n';
return std::nullopt;
}
cfg.hurt.duration = node["hurt"]["duration"].get_value<float>();
cfg.hurt.blink_hz = node["hurt"]["blink_hz"].get_value<float>();
// 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<float>();
cfg.visual_thrust.scale_divisor = node["visual_thrust"]["scale_divisor"].get_value<float>();
// 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<float>();
return cfg;
} catch (const std::exception& e) {
std::cerr << "[PlayerConfig] Excepció parsejant: " << e.what() << '\n';
return std::nullopt;
}
}
+72
View File
@@ -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 <SDL3/SDL.h>
#include <optional>
#include <string>
#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<PlayerConfig>;
};
+28 -46
View File
@@ -9,6 +9,7 @@
#include <cmath>
#include <cstdint>
#include <iostream>
#include <utility>
#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);
}
+13 -12
View File
@@ -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<Ship, N>`); 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.