Merge branch 'feat/entities-yaml-player': configuració del player en YAML
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
name: player_ship
|
||||||
|
|
||||||
|
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
|
||||||
|
# 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)
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
#include "core/defaults/physics.hpp"
|
#include "core/defaults/physics.hpp"
|
||||||
#include "core/defaults/playfield.hpp"
|
#include "core/defaults/playfield.hpp"
|
||||||
#include "core/defaults/rendering.hpp"
|
#include "core/defaults/rendering.hpp"
|
||||||
#include "core/defaults/ship.hpp"
|
|
||||||
#include "core/defaults/starfield_parallax.hpp"
|
#include "core/defaults/starfield_parallax.hpp"
|
||||||
#include "core/defaults/title.hpp"
|
#include "core/defaults/title.hpp"
|
||||||
#include "core/defaults/trail.hpp"
|
#include "core/defaults/trail.hpp"
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/defaults/entities.hpp"
|
|
||||||
|
|
||||||
namespace Defaults::Enemies {
|
namespace Defaults::Enemies {
|
||||||
|
|
||||||
// Cuerpo físico común (valores por defecto del constructor)
|
// Cuerpo físico común (valores por defecto del constructor)
|
||||||
@@ -78,10 +76,13 @@ namespace Defaults::Enemies {
|
|||||||
|
|
||||||
// Spawn safety and invulnerability system
|
// Spawn safety and invulnerability system
|
||||||
namespace Spawn {
|
namespace Spawn {
|
||||||
// Safe spawn distance from player
|
// Safe spawn distance from player. Antic: SHIP_RADIUS(12) * 3 = 36 px.
|
||||||
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius
|
// SHIP_RADIUS ha migrat al YAML del player; aquesta constant es
|
||||||
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
|
// mantindrà fixa fins al PR de migració dels enemics a YAML, on
|
||||||
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
|
// 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
|
// Invulnerability system
|
||||||
constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds
|
constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Defaults::Entities {
|
|||||||
constexpr int MAX_ORNIS = 15;
|
constexpr int MAX_ORNIS = 15;
|
||||||
constexpr int MAX_BULLETS = 50;
|
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 ENEMY_RADIUS = 20.0F;
|
||||||
constexpr float BULLET_RADIUS = 3.0F;
|
constexpr float BULLET_RADIUS = 3.0F;
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace Defaults::Game {
|
|||||||
// Friendly fire system
|
// Friendly fire system
|
||||||
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
||||||
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
|
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)
|
// Transición LEVEL_START (mensajes aleatorios PRE-level)
|
||||||
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
|
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
namespace Defaults::Physics {
|
namespace Defaults::Physics {
|
||||||
|
|
||||||
constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s)
|
// NOTA: els paràmetres específics de la nau del player (rotation_speed,
|
||||||
constexpr float ACCELERATION = 400.0F; // px/s²
|
// acceleration, max_velocity, death_impact_factor) viuen ara a
|
||||||
constexpr float MAX_VELOCITY = 180.0F; // px/s
|
// data/entities/player/player.yaml. La migració d'aquests fitxers va
|
||||||
constexpr float FRICTION = 20.0F; // px/s²
|
// començar amb la nau; els enemics i les bales són els següents.
|
||||||
|
|
||||||
// Bullet — impacto físico contra enemigo (impulse mass-aware).
|
// Bullet — impacto físico contra enemigo (impulse mass-aware).
|
||||||
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per
|
// 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
|
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
|
||||||
} // namespace Bullet
|
} // 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)
|
// Explosions (debris physics)
|
||||||
namespace Debris {
|
namespace Debris {
|
||||||
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
|
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -7,7 +7,7 @@ namespace Defaults::Trail {
|
|||||||
|
|
||||||
constexpr int POOL_SIZE = 200;
|
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_INTERVAL_S = 0.04F; // ~25 Hz nominal
|
||||||
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
|
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
|
||||||
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
|
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// entity_loader.cpp - Implementació del carregador d'entitats YAML
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/entities/entity_loader.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
|
||||||
|
namespace Entities {
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> EntityLoader::cache;
|
||||||
|
|
||||||
|
auto EntityLoader::load(const std::string& name) -> std::shared_ptr<fkyaml::node> {
|
||||||
|
// 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<uint8_t> 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>(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
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// entity_loader.hpp - Carregador genèric de descriptors d'entitats en YAML
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Cada entitat viu a `data/entities/<name>/<name>.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 <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#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<fkyaml::node>;
|
||||||
|
|
||||||
|
// Buidar caché (útil per debug/recàrrega).
|
||||||
|
static void clearCache();
|
||||||
|
|
||||||
|
[[nodiscard]] static auto getCacheSize() -> size_t;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Entities
|
||||||
@@ -53,7 +53,7 @@ void Bullet::init() {
|
|||||||
body_.clearAccumulators();
|
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
|
// Activar bullet
|
||||||
is_active_ = true;
|
is_active_ = true;
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id) {
|
|||||||
body_.angle = angle;
|
body_.angle = angle;
|
||||||
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
|
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
|
||||||
const float DIR_Y = std::sin(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_.angular_velocity = 0.0F;
|
||||||
body_.clearAccumulators();
|
body_.clearAccumulators();
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Bullet : public Entities::Entity {
|
|||||||
explicit Bullet(Rendering::Renderer* renderer);
|
explicit Bullet(Rendering::Renderer* renderer);
|
||||||
|
|
||||||
void init() override;
|
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 update(float delta_time) override;
|
||||||
void postUpdate(float delta_time) override;
|
void postUpdate(float delta_time) override;
|
||||||
void draw() const override;
|
void draw() const override;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/defaults.hpp"
|
#include "core/defaults.hpp"
|
||||||
@@ -20,27 +21,26 @@
|
|||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
#include "game/constants.hpp"
|
#include "game/constants.hpp"
|
||||||
|
|
||||||
Ship::Ship(Rendering::Renderer* renderer, const char* shape_file)
|
Ship::Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape_override)
|
||||||
: Entity(renderer) {
|
: Entity(renderer),
|
||||||
// Brightness específico para naves
|
config_(std::move(config)) {
|
||||||
brightness_ = Defaults::Brightness::NAU;
|
brightness_ = Defaults::Brightness::NAU;
|
||||||
|
|
||||||
// Configuración del cuerpo físico
|
body_.setMass(config_.physics.mass);
|
||||||
body_.setMass(Defaults::Ship::MASS);
|
body_.radius = config_.physics.collision_radius;
|
||||||
body_.radius = Defaults::Entities::SHIP_RADIUS;
|
body_.restitution = config_.physics.restitution;
|
||||||
body_.restitution = Defaults::Ship::RESTITUTION;
|
body_.linear_damping = config_.physics.linear_damping;
|
||||||
body_.linear_damping = Defaults::Ship::LINEAR_DAMPING;
|
body_.angular_damping = config_.physics.angular_damping;
|
||||||
body_.angular_damping = Defaults::Ship::ANGULAR_DAMPING;
|
|
||||||
|
|
||||||
// Cargar shape compartida desde archivo
|
// El shape pot venir del YAML o ser overridden (ex: P2 amb "ship2.shp").
|
||||||
shape_ = Graphics::ShapeLoader::load(shape_file);
|
const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path;
|
||||||
|
shape_ = Graphics::ShapeLoader::load(SHAPE_PATH);
|
||||||
if (!shape_ || !shape_->isValid()) {
|
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) {
|
void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) {
|
||||||
// Posición inicial
|
|
||||||
if (spawn_point != nullptr) {
|
if (spawn_point != nullptr) {
|
||||||
center_ = *spawn_point;
|
center_ = *spawn_point;
|
||||||
} else {
|
} else {
|
||||||
@@ -50,34 +50,27 @@ void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) {
|
|||||||
center_ = {.x = center_x, .y = center_y};
|
center_ = {.x = center_x, .y = center_y};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset orientación
|
|
||||||
angle_ = 0.0F;
|
angle_ = 0.0F;
|
||||||
|
|
||||||
// Sincronizar cuerpo físico con la posición/orientación inicial
|
|
||||||
body_.position = center_;
|
body_.position = center_;
|
||||||
body_.angle = angle_;
|
body_.angle = angle_;
|
||||||
body_.velocity = Vec2{};
|
body_.velocity = Vec2{};
|
||||||
body_.angular_velocity = 0.0F;
|
body_.angular_velocity = 0.0F;
|
||||||
body_.clearAccumulators();
|
body_.clearAccumulators();
|
||||||
|
|
||||||
// Activar invulnerabilidad solo si es respawn
|
invulnerable_timer_ = activar_invulnerabilitat ? config_.invulnerability.duration : 0.0F;
|
||||||
invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F;
|
|
||||||
is_hit_ = false;
|
is_hit_ = false;
|
||||||
hurt_timer_ = 0.0F;
|
hurt_timer_ = 0.0F;
|
||||||
touching_enemy_prev_frame_ = false;
|
touching_enemy_prev_frame_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Ship::processInput(float delta_time, uint8_t player_id) {
|
void Ship::processInput(float delta_time, uint8_t player_id) {
|
||||||
// Solo procesa input si la nave está viva
|
|
||||||
if (is_hit_) {
|
if (is_hit_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* input = Input::get();
|
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)
|
const bool ROTATE_RIGHT = (player_id == 0)
|
||||||
? input->checkActionPlayer1(InputAction::RIGHT, Input::ALLOW_REPEAT)
|
? input->checkActionPlayer1(InputAction::RIGHT, Input::ALLOW_REPEAT)
|
||||||
: input->checkActionPlayer2(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);
|
: input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT);
|
||||||
|
|
||||||
if (ROTATE_RIGHT) {
|
if (ROTATE_RIGHT) {
|
||||||
body_.angle += Defaults::Physics::ROTATION_SPEED * delta_time;
|
body_.angle += config_.physics.rotation_speed * delta_time;
|
||||||
}
|
}
|
||||||
if (ROTATE_LEFT) {
|
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.
|
// 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) {
|
if (THRUST) {
|
||||||
const float DIR_X = std::cos(body_.angle - (Constants::PI / 2.0F));
|
const float DIR_X = std::cos(body_.angle - (Constants::PI / 2.0F));
|
||||||
const float DIR_Y = std::sin(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 * config_.physics.acceleration;
|
||||||
const float MAGNITUDE = body_.mass * Defaults::Physics::ACCELERATION;
|
|
||||||
body_.applyForce(Vec2{.x = DIR_X * MAGNITUDE, .y = DIR_Y * MAGNITUDE});
|
body_.applyForce(Vec2{.x = DIR_X * MAGNITUDE, .y = DIR_Y * MAGNITUDE});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Ship::update(float delta_time) {
|
void Ship::update(float delta_time) {
|
||||||
// Solo update si la nave está viva
|
|
||||||
if (is_hit_) {
|
if (is_hit_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrementar timer de invulnerabilidad
|
|
||||||
if (invulnerable_timer_ > 0.0F) {
|
if (invulnerable_timer_ > 0.0F) {
|
||||||
invulnerable_timer_ -= delta_time;
|
invulnerable_timer_ -= delta_time;
|
||||||
invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F);
|
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) {
|
if (hurt_timer_ > 0.0F) {
|
||||||
hurt_timer_ -= delta_time;
|
hurt_timer_ -= delta_time;
|
||||||
hurt_timer_ = std::max(hurt_timer_, 0.0F);
|
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
|
// Cap de velocidad: el thrust acumula fuerza sin límite; limitamos
|
||||||
// la magnitud de body_.velocity tras aplicar fuerzas para preservar
|
// la magnitud de body_.velocity tras aplicar fuerzas para preservar
|
||||||
// el feel arcade del MAX_VELOCITY original.
|
// el feel arcade del MAX_VELOCITY original.
|
||||||
const float CURRENT_SPEED = body_.velocity.length();
|
const float CURRENT_SPEED = body_.velocity.length();
|
||||||
if (CURRENT_SPEED > Defaults::Physics::MAX_VELOCITY) {
|
if (CURRENT_SPEED > config_.physics.max_velocity) {
|
||||||
body_.velocity = body_.velocity * (Defaults::Physics::MAX_VELOCITY / CURRENT_SPEED);
|
body_.velocity = body_.velocity * (config_.physics.max_velocity / CURRENT_SPEED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Ship::postUpdate(float /*delta_time*/) {
|
void Ship::postUpdate(float /*delta_time*/) {
|
||||||
// Sincronizar mirror desde body_ tras la integración del world.
|
|
||||||
center_ = body_.position;
|
center_ = body_.position;
|
||||||
angle_ = body_.angle;
|
angle_ = body_.angle;
|
||||||
}
|
}
|
||||||
@@ -147,11 +132,10 @@ void Ship::draw() const {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parpadeo si invulnerable
|
|
||||||
if (isInvulnerable()) {
|
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);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,19 +145,17 @@ void Ship::draw() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Efecto visual de empuje: escala proporcional a la velocidad.
|
// 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 SPEED = getSpeed();
|
||||||
const float VISUAL_PUSH = SPEED / Defaults::Ship::VISUAL_PUSH_DIVISOR;
|
const float VISUAL_PUSH = SPEED / config_.visual_thrust.push_divisor;
|
||||||
const float SCALE = 1.0F + (VISUAL_PUSH / Defaults::Ship::VISUAL_SCALE_DIVISOR);
|
const float SCALE = 1.0F + (VISUAL_PUSH / config_.visual_thrust.scale_divisor);
|
||||||
|
|
||||||
// Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt
|
// 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 = config_.colors.normal;
|
||||||
SDL_Color color = color_normal_;
|
|
||||||
if (hurt_timer_ > 0.0F) {
|
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);
|
const float T = std::fmod(hurt_timer_, CYCLE);
|
||||||
if (T < (CYCLE / 2.0F)) {
|
if (T < (CYCLE / 2.0F)) {
|
||||||
color = color_hurt_;
|
color = config_.colors.hurt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +163,6 @@ void Ship::draw() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Ship::hurt() {
|
void Ship::hurt() {
|
||||||
hurt_timer_ = Defaults::Ship::Hurt::DURATION;
|
hurt_timer_ = config_.hurt.duration;
|
||||||
Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME);
|
Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,16 @@
|
|||||||
#include "core/defaults.hpp"
|
#include "core/defaults.hpp"
|
||||||
#include "core/entities/entity.hpp"
|
#include "core/entities/entity.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
|
#include "game/entities/player_config.hpp"
|
||||||
|
|
||||||
class Ship : public Entities::Entity {
|
class Ship : public Entities::Entity {
|
||||||
public:
|
public:
|
||||||
Ship()
|
Ship()
|
||||||
: Entity(nullptr) {}
|
: 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() override { init(nullptr, false); }
|
||||||
void init(const Vec2* spawn_point, bool activar_invulnerabilitat = 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
|
// Override: Interfaz de colisión
|
||||||
[[nodiscard]] auto getCollisionRadius() const -> float override {
|
[[nodiscard]] auto getCollisionRadius() const -> float override {
|
||||||
return Defaults::Entities::SHIP_RADIUS;
|
return config_.physics.collision_radius;
|
||||||
}
|
}
|
||||||
[[nodiscard]] auto isCollidable() const -> bool override {
|
[[nodiscard]] auto isCollidable() const -> bool override {
|
||||||
return !is_hit_ && invulnerable_timer_ <= 0.0F;
|
return !is_hit_ && invulnerable_timer_ <= 0.0F;
|
||||||
@@ -36,10 +40,9 @@ class Ship : public Entities::Entity {
|
|||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
[[nodiscard]] auto isInvulnerable() const -> bool { return invulnerable_timer_ > 0.0F; }
|
[[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; }
|
[[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 getSpeed() const -> float { return body_.velocity.length(); }
|
||||||
|
[[nodiscard]] auto getConfig() const -> const PlayerConfig& { return config_; }
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
void setCenter(const Vec2& nou_centre) {
|
void setCenter(const Vec2& nou_centre) {
|
||||||
@@ -65,17 +68,15 @@ class Ship : public Entities::Entity {
|
|||||||
void setTouchingEnemyPrevFrame(bool touching) { touching_enemy_prev_frame_ = touching; }
|
void setTouchingEnemyPrevFrame(bool touching) { touching_enemy_prev_frame_ = touching; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
|
// Configuració carregada des de YAML. Default-init zero permet el ctor
|
||||||
// Inicializados en la declaración: el ctor por defecto deja la nave "viva y sin invulnerabilidad",
|
// per defecte (necessari per a `std::array<Ship, N>`); s'omple via
|
||||||
// que es el estado coherente al que llevan tanto init() como el ctor con renderer.
|
// copy/move-assignment quan GameScene crea la nau real.
|
||||||
|
PlayerConfig config_{};
|
||||||
|
|
||||||
bool is_hit_{false};
|
bool is_hit_{false};
|
||||||
float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable
|
float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable
|
||||||
|
|
||||||
// Colors de la nau (propietats, prep per migració a YAML).
|
// >0 → estat HURT (parpelleig color normal ↔ color hurt).
|
||||||
SDL_Color color_normal_{Defaults::Palette::SHIP};
|
|
||||||
SDL_Color color_hurt_{Defaults::Palette::WOUNDED};
|
|
||||||
|
|
||||||
// >0 → estat HURT (parpelleig color_normal_ ↔ color_hurt_).
|
|
||||||
float hurt_timer_{0.0F};
|
float hurt_timer_{0.0F};
|
||||||
|
|
||||||
// Edge-trigger: true si el frame anterior la nau ja estava en contacte amb un enemic.
|
// Edge-trigger: true si el frame anterior la nau ja estava en contacte amb un enemic.
|
||||||
|
|||||||
@@ -10,10 +10,12 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/entities/entity_loader.hpp"
|
||||||
#include "core/input/input.hpp"
|
#include "core/input/input.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/system/scene_context.hpp"
|
#include "core/system/scene_context.hpp"
|
||||||
#include "core/system/service_menu.hpp"
|
#include "core/system/service_menu.hpp"
|
||||||
|
#include "game/entities/player_config.hpp"
|
||||||
#include "game/stage_system/stage_loader.hpp"
|
#include "game/stage_system/stage_loader.hpp"
|
||||||
#include "game/systems/collision_system.hpp"
|
#include "game/systems/collision_system.hpp"
|
||||||
#include "game/systems/continue_system.hpp"
|
#include "game/systems/continue_system.hpp"
|
||||||
@@ -49,9 +51,22 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
auto option = context_.consumeOption();
|
auto option = context_.consumeOption();
|
||||||
(void)option; // Suprimir warning de variable no usada
|
(void)option; // Suprimir warning de variable no usada
|
||||||
|
|
||||||
// Inicialitzar naves con renderer (P1=ship.shp, P2=ship2.shp)
|
// Carregar la configuració del player des de YAML. Sense fallback: si
|
||||||
ships_[0] = Ship(sdl.getRenderer(), "ship.shp"); // Jugador 1: nave estàndar
|
// falla, abortem (la nau no és construïble sense paràmetres).
|
||||||
ships_[1] = Ship(sdl.getRenderer(), "ship2.shp"); // Jugador 2: interceptor con ales
|
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
|
// Inicialitzar balas con renderer
|
||||||
std::ranges::fill(bullets_, Bullet(sdl.getRenderer()));
|
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;
|
const int START_IDX = player_id * SLOTS_PER_PLAYER;
|
||||||
for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) {
|
for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) {
|
||||||
if (!bullets_[i].isActive()) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,7 +224,8 @@ namespace Systems::Collision {
|
|||||||
// Segon impacte durant HURT → mort. Aplica un impuls afegit
|
// Segon impacte durant HURT → mort. Aplica un impuls afegit
|
||||||
// perquè l'enemic surti disparat (feedback visible).
|
// perquè l'enemic surti disparat (feedback visible).
|
||||||
const Vec2 SHIP_VEL = ctx.ships[i].getVelocityVector();
|
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);
|
touched_enemy->applyImpulse(IMPULSE);
|
||||||
ctx.on_player_hit(i);
|
ctx.on_player_hit(i);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user