Compare commits

..

4 Commits

21 changed files with 710 additions and 438 deletions
+4 -1
View File
@@ -110,7 +110,10 @@ add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
) )
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source") target_include_directories(pack_resources PRIVATE
"${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack --- # --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
+43
View File
@@ -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)
-1
View File
@@ -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"
+7 -6
View File
@@ -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
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
+4 -12
View File
@@ -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)
-33
View File
@@ -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
+1 -1
View File
@@ -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
+56
View File
@@ -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
+38
View File
@@ -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
+238 -251
View File
@@ -10,300 +10,287 @@
namespace Resource { namespace Resource {
// Calcular checksum CRC32 simplificat // Calcular checksum CRC32 simplificat
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t { auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
uint32_t checksum = 0x12345678; uint32_t checksum = 0x12345678;
for (unsigned char byte : data) { for (unsigned char byte : data) {
checksum = ((checksum << 5) + checksum) + byte; checksum = ((checksum << 5) + checksum) + byte;
} }
return checksum; return checksum;
}
// Encriptació XOR (simètrica)
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) {
return;
}
for (size_t i = 0; i < data.size(); ++i) {
data[i] ^= key[i % key.length()];
}
}
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
// XOR es simètric
encryptData(data, key);
}
// Llegir file complet a memòria
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
return {};
} }
std::streamsize file_size = file.tellg(); // Encriptació XOR (simètrica)
file.seekg(0, std::ios::beg); void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) {
std::vector<uint8_t> data(file_size); return;
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) { }
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n'; for (size_t i = 0; i < data.size(); ++i) {
return {}; data[i] ^= key[i % key.length()];
}
} }
return data; void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
} // XOR es simètric
encryptData(data, key);
// Añadir un file individual al paquet
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
auto file_data = readFile(filepath);
if (file_data.empty()) {
return false;
} }
ResourceEntry entry{ // Llegir file complet a memòria
.filename = pack_name, auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
.offset = data_.size(), std::ifstream file(filepath, std::ios::binary | std::ios::ate);
.size = file_data.size(), if (!file) {
.checksum = calculateChecksum(file_data)}; std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
return {};
// Añadir dades al bloc de dades
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[pack_name] = entry;
std::cout << "[ResourcePack] Añadido: " << pack_name << " (" << file_data.size()
<< " bytes)\n";
return true;
}
// Añadir todos los archivos de un directori recursivament
auto Pack::addDirectory(const std::string& dir_path,
const std::string& base_path) -> bool {
namespace fs = std::filesystem;
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
return false;
}
std::string current_base = base_path.empty() ? "" : base_path + "/";
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
if (!entry.is_regular_file()) {
continue;
} }
std::string full_path = entry.path().string(); std::streamsize file_size = file.tellg();
std::string relative_path = entry.path().lexically_relative(dir_path).string(); file.seekg(0, std::ios::beg);
// Convertir barres invertides a normals (Windows) std::vector<uint8_t> data(file_size);
std::ranges::replace(relative_path, '\\', '/'); if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n';
// Saltar archivos de desenvolupament return {};
if (relative_path.find(".world") != std::string::npos ||
relative_path.find(".tsx") != std::string::npos ||
relative_path.find(".DS_Store") != std::string::npos ||
relative_path.find(".git") != std::string::npos) {
std::cout << "[ResourcePack] Saltant: " << relative_path << '\n';
continue;
} }
std::string pack_name = current_base + relative_path; return data;
addFile(full_path, pack_name);
} }
return true; // Añadir un file individual al paquet
} auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
auto file_data = readFile(filepath);
if (file_data.empty()) {
return false;
}
// Guardar paquet a disc ResourceEntry entry{
auto Pack::savePack(const std::string& pack_file) -> bool { .filename = pack_name,
std::ofstream file(pack_file, std::ios::binary); .offset = data_.size(),
if (!file) { .size = file_data.size(),
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n'; .checksum = calculateChecksum(file_data)};
return false;
// Añadir dades al bloc de dades
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[pack_name] = entry;
return true;
} }
// Escriure capçalera // Añadir todos los archivos de un directori recursivament
file.write(MAGIC_HEADER, 4); auto Pack::addDirectory(const std::string& dir_path,
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION)); const std::string& base_path) -> bool {
namespace fs = std::filesystem;
// Escriure nombre de recursos if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
auto resource_count = static_cast<uint32_t>(resources_.size()); std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count)); return false;
}
// Escriure metadades de recursos std::string current_base = base_path.empty() ? "" : base_path + "/";
for (const auto& [name, entry] : resources_) {
// Nom del file
auto name_len = static_cast<uint32_t>(entry.filename.length());
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
file.write(entry.filename.c_str(), name_len);
// Offset, mida, checksum for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset)); if (!entry.is_regular_file()) {
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size)); continue;
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum)); }
std::string full_path = entry.path().string();
std::string relative_path = entry.path().lexically_relative(dir_path).string();
// Convertir barres invertides a normals (Windows)
std::ranges::replace(relative_path, '\\', '/');
// Saltar archivos de desenvolupament
if (relative_path.find(".world") != std::string::npos ||
relative_path.find(".tsx") != std::string::npos ||
relative_path.find(".DS_Store") != std::string::npos ||
relative_path.find(".git") != std::string::npos) {
continue;
}
std::string pack_name = current_base + relative_path;
addFile(full_path, pack_name);
}
return true;
} }
// Encriptar dades // Guardar paquet a disc
std::vector<uint8_t> encrypted_data = data_; auto Pack::savePack(const std::string& pack_file) -> bool {
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY); std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n';
return false;
}
// Escriure mida de dades y dades encriptades // Escriure capçalera
auto data_size = static_cast<uint64_t>(encrypted_data.size()); file.write(MAGIC_HEADER, 4);
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size)); file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
std::cout << "[ResourcePack] Guardat: " << pack_file << " (" << resources_.size() // Escriure nombre de recursos
<< " recursos, " << data_size << " bytes)\n"; auto resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
return true; // Escriure metadades de recursos
} for (const auto& [name, entry] : resources_) {
// Nom del file
auto name_len = static_cast<uint32_t>(entry.filename.length());
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
file.write(entry.filename.c_str(), name_len);
// Carregar paquet desde disc // Offset, mida, checksum
auto Pack::loadPack(const std::string& pack_file) -> bool { file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
std::ifstream file(pack_file, std::ios::binary); file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
if (!file) { file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n'; }
return false;
// Encriptar dades
std::vector<uint8_t> encrypted_data = data_;
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
// Escriure mida de dades y dades encriptades
auto data_size = static_cast<uint64_t>(encrypted_data.size());
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
return true;
} }
// Llegir capçalera // Carregar paquet desde disc
char magic[4]; auto Pack::loadPack(const std::string& pack_file) -> bool {
file.read(magic, 4); std::ifstream file(pack_file, std::ios::binary);
if (std::string(magic, 4) != MAGIC_HEADER) { if (!file) {
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
<< ")\n"; return false;
return false; }
// Llegir capçalera
char magic[4];
file.read(magic, 4);
if (std::string(magic, 4) != MAGIC_HEADER) {
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER
<< ")\n";
return false;
}
uint32_t version;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) {
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION
<< ", trobat " << version << ")\n";
return false;
}
// Llegir nombre de recursos
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
// Llegir metadades de recursos
resources_.clear();
for (uint32_t i = 0; i < resource_count; ++i) {
// Nom del file
uint32_t name_len;
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
std::string filename(name_len, '\0');
file.read(filename.data(), name_len);
// Offset, mida, checksum
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
// Llegir dades encriptades
uint64_t data_size;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), data_size);
// Desencriptar
decryptData(data_, DEFAULT_ENCRYPT_KEY);
return true;
} }
uint32_t version; // Obtenir un recurs del paquet
file.read(reinterpret_cast<char*>(&version), sizeof(version)); auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
if (version != VERSION) { auto it = resources_.find(filename);
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION if (it == resources_.end()) {
<< ", trobat " << version << ")\n"; std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
return false; return {};
} }
// Llegir nombre de recursos const auto& entry = it->second;
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
// Llegir metadades de recursos // Extreure dades
resources_.clear();
for (uint32_t i = 0; i < resource_count; ++i) {
// Nom del file
uint32_t name_len;
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
std::string filename(name_len, '\0');
file.read(filename.data(), name_len);
// Offset, mida, checksum
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
// Llegir dades encriptades
uint64_t data_size;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), data_size);
// Desencriptar
decryptData(data_, DEFAULT_ENCRYPT_KEY);
std::cout << "[ResourcePack] Carregat: " << pack_file << " (" << resources_.size()
<< " recursos)\n";
return true;
}
// Obtenir un recurs del paquet
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
auto it = resources_.find(filename);
if (it == resources_.end()) {
std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
return {};
}
const auto& entry = it->second;
// Extreure dades
if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
return {};
}
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
// Verificar checksum
uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum
<< ")\n";
// No falla, pero adverteix
}
return resource_data;
}
// Comprovar si existeix un recurs
auto Pack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename);
}
// Obtenir list de todos los recursos
auto Pack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> list;
list.reserve(resources_.size());
for (const auto& [name, entry] : resources_) {
list.push_back(name);
}
std::ranges::sort(list);
return list;
}
// Validar integritat del paquet
auto Pack::validatePack() const -> bool {
bool valid = true;
for (const auto& [name, entry] : resources_) {
// Verificar offset i mida
if (entry.offset + entry.size > data_.size()) { if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error de validació: " << name std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
<< " té offset/mida invàlid\n"; return {};
valid = false;
continue;
} }
// Extreure i verificar checksum
std::vector<uint8_t> resource_data(data_.begin() + entry.offset, std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size); data_.begin() + entry.offset + entry.size);
// Verificar checksum
uint32_t computed_checksum = calculateChecksum(resource_data); uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) { if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] Error de validació: " << name std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
<< " té checksum invàlid\n"; << " (esperat " << entry.checksum << ", calculat " << computed_checksum
valid = false; << ")\n";
// No falla, pero adverteix
} }
return resource_data;
} }
if (valid) { // Comprovar si existeix un recurs
std::cout << "[ResourcePack] Validació OK (" << resources_.size() << " recursos)\n"; auto Pack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename);
} }
return valid; // Obtenir list de todos los recursos
} auto Pack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> list;
list.reserve(resources_.size());
for (const auto& [name, entry] : resources_) {
list.push_back(name);
}
std::ranges::sort(list);
return list;
}
// Validar integritat del paquet
auto Pack::validatePack() const -> bool {
bool valid = true;
for (const auto& [name, entry] : resources_) {
// Verificar offset i mida
if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error de validació: " << name
<< " té offset/mida invàlid\n";
valid = false;
continue;
}
// Extreure i verificar checksum
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] Error de validació: " << name
<< " té checksum invàlid\n";
valid = false;
}
}
return valid;
}
} // namespace Resource } // namespace Resource
+2 -2
View File
@@ -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();
+1 -1
View File
@@ -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;
+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 <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);
} }
+13 -12
View File
@@ -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.
+19 -4
View File
@@ -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;
} }
} }
+2 -1
View File
@@ -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 {
+72 -65
View File
@@ -1,92 +1,99 @@
// pack_resources.cpp - Utilitat per crear paquets de recursos // pack_resources.cpp - Utilitat per crear paquets de recursos
// © 2026 JailDesigner // © 2026 JailDesigner
#include "../../source/core/resources/resource_pack.hpp"
#include <filesystem> #include <filesystem>
#include <iostream> #include <iostream>
#include <string>
void print_usage(const char* program_name) { #include "core/resources/resource_pack.hpp"
std::cout << "Ús: " << program_name << " [opcions] [directori_entrada] [fitxer_sortida]\n"; #include "project.h"
std::cout << "\nOpcions:\n";
std::cout << " --list <fitxer> Llistar contingut d'un paquet\n"; namespace {
std::cout << "\nExemples:\n";
std::cout << " " << program_name << " data resources.pack\n"; void showHelp() {
std::cout << " " << program_name << " --list resources.pack\n"; std::cout << Project::LONG_NAME << " - Resource Packer\n";
std::cout << "\nSi no s'especifiquen arguments, empaqueta 'data/' a 'resources.pack'\n"; std::cout << "==============================================\n";
} std::cout << "Usage: pack_resources [options] [input_dir] [output_file]\n\n";
std::cout << "Options:\n";
std::cout << " --help Show this help message\n";
std::cout << " --list List contents of an existing pack file\n\n";
std::cout << "Arguments:\n";
std::cout << " input_dir Directory to pack (default: data)\n";
std::cout << " output_file Pack file name (default: resources.pack)\n";
}
void listPackContents(const std::string& pack_file) {
Resource::Pack pack;
if (!pack.loadPack(pack_file)) {
std::cerr << "Error: cannot open pack file: " << pack_file << '\n';
return;
}
auto resources = pack.getResourceList();
std::cout << "Pack file: " << pack_file << '\n';
std::cout << "Resources: " << resources.size() << '\n';
for (const auto& r : resources) { std::cout << " " << r << '\n'; }
}
} // namespace
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
std::string input_dir = "data"; std::string data_dir = "data";
std::string output_file = "resources.pack"; std::string output_file = "resources.pack";
bool list_mode = false;
bool data_dir_set = false;
// Processar arguments for (int i = 1; i < argc; ++i) {
if (argc == 2 && std::string(argv[1]) == "--help") { std::string arg = argv[i];
print_usage(argv[0]); if (arg == "--help" || arg == "-h") {
showHelp();
return 0;
}
if (arg == "--list") {
list_mode = true;
if (i + 1 < argc) { output_file = argv[++i]; }
continue;
}
if (!arg.empty() && arg[0] != '-') {
if (!data_dir_set) {
data_dir = arg;
data_dir_set = true;
} else {
output_file = arg;
}
}
}
if (list_mode) {
listPackContents(output_file);
return 0; return 0;
} }
// Mode --list std::cout << Project::LONG_NAME << " - Resource Packer\n";
if (argc == 3 && std::string(argv[1]) == "--list") { std::cout << "==============================================\n";
Resource::Pack pack; std::cout << "Input directory: " << data_dir << '\n';
if (!pack.loadPack(argv[2])) { std::cout << "Output file: " << output_file << '\n';
std::cerr << "ERROR: No es pot carregar " << argv[2] << "\n";
return 1;
}
std::cout << "Contingut de " << argv[2] << ":\n"; if (!std::filesystem::exists(data_dir)) {
auto resources = pack.getResourceList(); std::cerr << "Error: input directory does not exist: " << data_dir << '\n';
std::cout << "Total: " << resources.size() << " recursos\n\n";
for (const auto& name : resources) {
std::cout << " " << name << "\n";
}
return 0;
}
// Mode empaquetar
if (argc >= 3) {
input_dir = argv[1];
output_file = argv[2];
}
// Verificar que existeix el directori
if (!std::filesystem::exists(input_dir)) {
std::cerr << "ERROR: Directori no trobat: " << input_dir << "\n";
return 1; return 1;
} }
if (!std::filesystem::is_directory(input_dir)) {
std::cerr << "ERROR: " << input_dir << " no és un directori\n";
return 1;
}
// Crear paquet
std::cout << "Creant paquet de recursos...\n";
std::cout << " Entrada: " << input_dir << "\n";
std::cout << " Sortida: " << output_file << "\n\n";
Resource::Pack pack; Resource::Pack pack;
if (!pack.addDirectory(input_dir)) { std::cout << "Scanning and packing resources...\n";
std::cerr << "ERROR: No s'ha pogut afegir el directori\n"; if (!pack.addDirectory(data_dir)) {
std::cerr << "Error: failed to add directory to pack\n";
return 1; return 1;
} }
std::cout << "Found " << pack.getResourceList().size() << " resources\n";
std::cout << "Saving pack file...\n";
if (!pack.savePack(output_file)) { if (!pack.savePack(output_file)) {
std::cerr << "ERROR: No s'ha pogut guardar el paquet\n"; std::cerr << "Error: failed to save pack file\n";
return 1; return 1;
} }
// Resum auto file_size = std::filesystem::file_size(std::filesystem::path(output_file));
auto resources = pack.getResourceList(); std::cout << "Pack file created: " << output_file << " ("
std::cout << "\n"; << (static_cast<double>(file_size) / 1024.0 / 1024.0) << " MB)\n";
std::cout << "✓ Paquet creat amb èxit!\n";
std::cout << " Recursos: " << resources.size() << "\n";
// Mostrar mida del fitxer
auto file_size = std::filesystem::file_size(output_file);
std::cout << " Mida: " << (file_size / 1024) << " KB\n";
return 0; return 0;
} }