Merge branch 'feat/enemy-death': muerte d'enemics amb herida prèvia + debris físic

This commit is contained in:
2026-05-21 17:16:16 +02:00
48 changed files with 8210 additions and 1147 deletions
+37 -34
View File
@@ -133,44 +133,47 @@ add_custom_command(
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V ---
# Compila tots els shaders .glsl a SPIR-V (Vulkan/Linux/Windows).
# macOS necessitarà MSL en el futur (Metal) — es generen amb spirv-cross
# o glslang amb target distint en una etapa posterior.
# Sortida: build/shaders/*.spv
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V (headers C++ embedits) ---
# Compila els shaders .glsl a SPIR-V i els converteix en headers C++ embedits
# (source/core/rendering/gpu/spv/*.h). Aquests headers es commiteen al repo:
# en macOS no cal glslc (els headers ja existeixen). En Linux/Windows glslc
# és obligatori per regenerar els headers en cada canvi del GLSL.
#
# Per a macOS hi ha a més els headers MSL escrits a mà a source/core/rendering/gpu/msl/.
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/shaders")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/gpu/spv")
set(ALL_SHADER_HEADERS
"${HEADERS_DIR}/line_vert_spv.h"
"${HEADERS_DIR}/line_frag_spv.h"
"${HEADERS_DIR}/postfx_vert_spv.h"
"${HEADERS_DIR}/postfx_frag_spv.h"
)
set(ALL_SHADER_SOURCES
"${SHADERS_DIR}/line.vert.glsl"
"${SHADERS_DIR}/line.frag.glsl"
"${SHADERS_DIR}/postfx.vert.glsl"
"${SHADERS_DIR}/postfx.frag.glsl"
)
find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
if(GLSLC_EXE)
file(GLOB SHADER_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/shaders/*.glsl")
set(COMPILED_SHADERS "")
foreach(SHADER ${SHADER_SOURCES})
get_filename_component(SHADER_NAME ${SHADER} NAME)
# Detectar stage del nom: line.vert.glsl → vert, line.frag.glsl → frag
if(SHADER_NAME MATCHES "\\.vert\\.glsl$")
set(SHADER_STAGE "vert")
string(REPLACE ".glsl" ".spv" SPV_NAME ${SHADER_NAME})
elseif(SHADER_NAME MATCHES "\\.frag\\.glsl$")
set(SHADER_STAGE "frag")
string(REPLACE ".glsl" ".spv" SPV_NAME ${SHADER_NAME})
else()
message(WARNING "Shader sense stage detectat: ${SHADER_NAME} (esperat .vert.glsl o .frag.glsl)")
continue()
endif()
set(SPV_OUTPUT "${CMAKE_BINARY_DIR}/shaders/${SPV_NAME}")
add_custom_command(
OUTPUT ${SPV_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/shaders"
COMMAND ${GLSLC_EXE} -fshader-stage=${SHADER_STAGE} -O ${SHADER} -o ${SPV_OUTPUT}
DEPENDS ${SHADER}
COMMENT "Compilant shader ${SHADER_NAME} → ${SPV_NAME}"
VERBATIM
)
list(APPEND COMPILED_SHADERS ${SPV_OUTPUT})
endforeach()
add_custom_target(shaders ALL DEPENDS ${COMPILED_SHADERS})
add_custom_command(
OUTPUT ${ALL_SHADER_HEADERS}
COMMAND ${CMAKE_COMMAND}
-D GLSLC=${GLSLC_EXE}
-D SHADERS_DIR=${SHADERS_DIR}
-D HEADERS_DIR=${HEADERS_DIR}
-P ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
DEPENDS ${ALL_SHADER_SOURCES} ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
COMMENT "Compilant shaders GLSL → headers SPIR-V embedits"
VERBATIM
)
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
add_dependencies(${PROJECT_NAME} shaders)
message(STATUS "Shaders trobats: ${SHADER_SOURCES}")
message(STATUS "Shaders: glslc trobat (${GLSLC_EXE}); headers SPV es regeneraran si canvia el GLSL")
elseif(APPLE)
message(STATUS "Shaders: glslc no trobat en macOS — s'usaran els headers SPV ja commiteats")
else()
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V")
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V (obligatori a Linux/Windows)")
endif()
# --- STATIC ANALYSIS / FORMAT TARGETS ---
+7
View File
@@ -27,6 +27,13 @@ namespace Config {
struct RenderingConfig {
int vsync{1}; // 0=disabled, 1=enabled
int antialias{1}; // 0=disabled, 1=enabled (AA geomètric a les línies, toggle F5)
// Resolució del render target offscreen (independent del tamany lògic
// 1280×720 del joc). Aquesta és la resolució real on rasteritzen les
// línies abans de l'escala final a la swapchain; pujar-la millora
// la nitidesa en finestres grans i fullscreen. Llista tancada de
// presets 16:9 — veure Defaults::Rendering::RESOLUTION_PRESETS.
int render_width{1280};
int render_height{720};
};
struct KeyboardBindings {
+4
View File
@@ -8,6 +8,7 @@
#pragma once
// IWYU pragma: begin_exports
#include "core/defaults/audio.hpp"
#include "core/defaults/brightness.hpp"
#include "core/defaults/controls.hpp"
@@ -15,7 +16,9 @@
#include "core/defaults/entities.hpp"
#include "core/defaults/floating_score.hpp"
#include "core/defaults/game.hpp"
#include "core/defaults/hud.hpp"
#include "core/defaults/math.hpp"
#include "core/defaults/notifier.hpp"
#include "core/defaults/palette.hpp"
#include "core/defaults/physics.hpp"
#include "core/defaults/rendering.hpp"
@@ -23,3 +26,4 @@
#include "core/defaults/title.hpp"
#include "core/defaults/window.hpp"
#include "core/defaults/zones.hpp"
// IWYU pragma: end_exports
+23 -5
View File
@@ -7,19 +7,30 @@
namespace Defaults::Enemies {
// Cuerpo físico común (valores por defecto del constructor)
namespace Body {
constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0)
constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes
constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad
constexpr float ANGULAR_DAMPING = 0.0F;
} // namespace Body
// Pentagon (esquivador - zigzag evasion)
namespace Pentagon {
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower)
constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad)
constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower)
constexpr float MASS = 5.0F; // Masa estándar
constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad)
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
} // namespace Pentagon
// Cuadrado (perseguidor - tracks player)
namespace Cuadrado {
constexpr float VELOCITAT = 40.0F; // px/s (medium speed)
constexpr float MASS = 8.0F; // Más pesado, "tanque"
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%]
@@ -30,6 +41,7 @@ namespace Defaults::Enemies {
// Molinillo (agressiu - fast straight lines, proximity spin-up)
namespace Molinillo {
constexpr float VELOCITAT = 50.0F; // px/s (fastest)
constexpr float MASS = 4.0F; // Más liviano, ágil
constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change)
constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments
constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%]
@@ -58,6 +70,12 @@ namespace Defaults::Enemies {
constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
} // namespace Animation
// Wounded state (entre primer impacto y explosión)
namespace Wounded {
constexpr float DURATION = 1.0F; // Segundos en estado herido antes de explotar
constexpr float BLINK_HZ = 10.0F; // Frecuencia de parpadeo color tipo ↔ dorado
} // namespace Wounded
// Spawn safety and invulnerability system
namespace Spawn {
// Safe spawn distance from player
+8 -3
View File
@@ -10,9 +10,13 @@ namespace Defaults::Game {
constexpr int HEIGHT = 720;
// Regles de partida
constexpr int STARTING_LIVES = 3; // Initial lives
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
constexpr int STARTING_LIVES = 3; // Initial lives
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
// Valores centinela del temporitzador de mort per-jugador.
constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu
constexpr float HIT_TIMER_TRIGGER_DEATH = 0.001F; // Trigger inicial post-impacte (>0 sense disparar regla)
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous)
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
@@ -20,6 +24,7 @@ namespace Defaults::Game {
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s)
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS
// Transición LEVEL_START (mensajes aleatorios PRE-level)
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
+41
View File
@@ -0,0 +1,41 @@
// hud.hpp - Configuració visual del HUD (marcador, etc.)
// © 2026 JailDesigner
#pragma once
namespace Defaults::Hud {
// Marcador (scoreboard inferior). Usado por GameScene::drawScoreboard()
// y por la animación de entrada en init_hud_animator.
constexpr float SCOREBOARD_TEXT_SCALE = 0.85F;
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
// Animación de entrada del HUD (init_hud_animator).
namespace InitAnim {
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
constexpr float SHIP_SPAWN_Y_OFFSET = 50.0F;
// Bordes: ratios de las tres fases (top → laterales → bottom).
constexpr float BORDER_PHASE_1_END = 0.33F; // Fin de la fase top
constexpr float BORDER_PHASE_2_END = 0.66F; // Fin de la fase laterales
} // namespace InitAnim
// Indicadores ("tips") sobre los enemigos enganchados a la nave.
// Offset local al frame de la nave (apunta hacia delante, eje Y negativo).
namespace Tips {
constexpr float LOCAL_X = 0.0F;
constexpr float LOCAL_Y = -12.0F;
} // namespace Tips
// Overlay de debug (FPS, métriques) en coordenades lògiques (1280×720).
namespace DebugOverlay {
constexpr float X = 12.0F;
constexpr float Y_FPS = 12.0F;
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt)
constexpr float TEXT_SCALE = 0.4F;
constexpr float TEXT_SPACING = 2.0F;
constexpr float BRIGHTNESS = 1.0F;
constexpr float FPS_UPDATE_INTERVAL = 0.5F; // Cadencia d'actualització del FPS visible
} // namespace DebugOverlay
} // namespace Defaults::Hud
+31
View File
@@ -0,0 +1,31 @@
// notifier.hpp - Configuració del cuadre de notificacions toast (System::Notifier)
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
namespace Defaults::Notifier {
// Geometria del cuadre en coordenades lògiques (1280×720).
constexpr float CANVAS_WIDTH = 1280.0F;
constexpr float MARGIN_TOP = 40.0F;
constexpr float PADDING_H = 16.0F;
constexpr float PADDING_V = 10.0F;
constexpr float BORDER_THICKNESS = 2.0F;
constexpr float TEXT_SCALE = 0.55F;
constexpr float TEXT_SPACING = 2.0F;
constexpr float BORDER_BRIGHTNESS = 1.0F;
// Cinemàtica del slide.
constexpr float SLIDE_DURATION_S = 0.30F;
// Presets per als atajos semàntics.
constexpr SDL_Color COLOR_INFO{.r = 80, .g = 230, .b = 255, .a = 255};
constexpr SDL_Color COLOR_WARN{.r = 255, .g = 180, .b = 40, .a = 255};
constexpr SDL_Color COLOR_EXIT{.r = 255, .g = 80, .b = 80, .a = 255};
constexpr float DURATION_INFO = 2.0F;
constexpr float DURATION_WARN = 3.0F;
constexpr float DURATION_EXIT = 3.0F;
} // namespace Defaults::Notifier
+1
View File
@@ -15,5 +15,6 @@ namespace Defaults::Palette {
constexpr SDL_Color PENTAGON = {.r = 120, .g = 170, .b = 255, .a = 255}; // Azul "esquivador"
constexpr SDL_Color QUADRAT = {.r = 255, .g = 110, .b = 110, .a = 255}; // Rojo "tank"
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 130, .b = 255, .a = 255}; // Magenta agresivo
constexpr SDL_Color WOUNDED = {.r = 255, .g = 215, .b = 0, .a = 255}; // Dorado: enemigo herido
} // namespace Defaults::Palette
+34 -3
View File
@@ -10,6 +10,14 @@ namespace Defaults::Physics {
constexpr float MAX_VELOCITY = 120.0F; // px/s
constexpr float FRICTION = 20.0F; // px/s²
// Bullet — impacto físico contra enemigo (impulse mass-aware).
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat.
namespace Bullet {
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
} // namespace Bullet
// Explosions (debris physics)
namespace Debris {
constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s)
@@ -17,15 +25,38 @@ namespace Defaults::Physics {
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
constexpr float ROTACIO_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
constexpr float TEMPS_VIDA = 2.0F; // Duració màxima (segons) - enemy/bullet debris
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor/s)
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
// Política de mort: passat el min_lifetime, el fragment mor quan la
// seva velocity cau per sota d'aquest llindar. Així els fragments
// ràpids no "popen" en moviment.
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
// 0.7 = 70% de l'energia conservada al rebot.
constexpr float RESTITUTION_BOUNDS = 0.7F;
// Herència de velocity angular (trayectorias curvas)
constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat
constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
// Velocity heredada de la nau a l'explosió (80% del feel original).
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
// Tuneig específic de l'explosió d'enemic (overrides als defaults
// que es passen com a paràmetres opcionals a explode()).
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
// Angular velocity sin for trajectory inheritance
// Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies
+31
View File
@@ -3,9 +3,40 @@
#pragma once
#include <algorithm>
#include <array>
namespace Defaults::Rendering {
constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled
constexpr int ANTIALIAS_DEFAULT = 1; // 0=disabled, 1=enabled (AA geomètric a les línies)
// Grosor global per defecte de les línies. 1.5 dóna línia visible i crujent;
// 1.0 es veu massa fi en pantalles grans. Configurable via setLineThickness.
constexpr float LINE_THICKNESS_DEFAULT = 1.5F;
// Resolució del render target offscreen. El tamany lògic del joc roman a
// 1280×720 (coordenades dels objectes); aquesta és la resolució física a
// la qual es rasteritzen les línies abans de la composició final.
struct ResolutionPreset {
int w;
int h;
};
constexpr std::array<ResolutionPreset, 5> RESOLUTION_PRESETS{{
{.w = 1280, .h = 720}, // HD 720p (default)
{.w = 1600, .h = 900}, // HD+ 900p
{.w = 1920, .h = 1080}, // Full HD 1080p
{.w = 2560, .h = 1440}, // QHD 1440p
{.w = 3840, .h = 2160} // 4K UHD 2160p
}};
constexpr int RENDER_WIDTH_DEFAULT = 1280;
constexpr int RENDER_HEIGHT_DEFAULT = 720;
constexpr auto isValidRenderResolution(int w, int h) -> bool {
return std::ranges::any_of(RESOLUTION_PRESETS,
[w, h](const ResolutionPreset& preset) { return preset.w == w && preset.h == h; });
}
} // namespace Defaults::Rendering
+11
View File
@@ -13,4 +13,15 @@ namespace Defaults::Ship {
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)
} // namespace Defaults::Ship
+3 -3
View File
@@ -50,10 +50,10 @@ Starfield::Starfield(Rendering::Renderer* renderer,
estrella.capa = capa_idx;
// Angle aleatori
estrella.angle = (static_cast<float>(rand()) / RAND_MAX) * 2.0F * Defaults::Math::PI;
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
// Distancia aleatòria (0.0 a 1.0) per omplir toda la pantalla
estrella.distancia_centre = static_cast<float>(rand()) / RAND_MAX;
estrella.distancia_centre = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
// Calcular posición desde la distancia
float radi = estrella.distancia_centre * radi_max_;
@@ -68,7 +68,7 @@ Starfield::Starfield(Rendering::Renderer* renderer,
// Inicialitzar una estrella (nueva o regenerada)
void Starfield::initStar(Estrella& estrella) const {
// Angle aleatori des del point de fuga hacia fuera
estrella.angle = (static_cast<float>(rand()) / RAND_MAX) * 2.0F * Defaults::Math::PI;
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
// Distancia inicial pequeña (5% del radi màxim) - neix prop del centro
estrella.distancia_centre = 0.05F;
+147 -140
View File
@@ -10,172 +10,179 @@
namespace Physics {
void PhysicsWorld::addBody(RigidBody* body) {
if (body == nullptr) {
return;
}
if (std::ranges::find(bodies_, body) == bodies_.end()) {
bodies_.push_back(body);
}
}
void PhysicsWorld::removeBody(RigidBody* body) {
std::erase(bodies_, body);
}
void PhysicsWorld::update(float dt) {
integrate(dt);
if (has_bounds_) {
resolveBoundsCollisions();
}
resolveBodyCollisions();
}
// Integración semi-implícita de Euler:
// v(t+dt) = v(t) + (F/m) * dt
// x(t+dt) = x(t) + v(t+dt) * dt
// Más estable que Euler explícito para juegos. Damping exponencial.
void PhysicsWorld::integrate(float dt) {
for (auto* body : bodies_) {
if (body == nullptr || body->isStatic()) {
continue;
void PhysicsWorld::addBody(RigidBody* body) {
if (body == nullptr) {
return;
}
// Aplicar fuerzas acumuladas → aceleración
const Vec2 ACCELERATION = body->force_accumulator * body->inverse_mass;
body->velocity += ACCELERATION * dt;
// Damping exponencial: equivalente a v *= exp(-damping * dt)
// Aproximación lineal cuando damping*dt es pequeño.
if (body->linear_damping > 0.0F) {
const float DAMP = std::exp(-body->linear_damping * dt);
body->velocity *= DAMP;
if (std::ranges::find(bodies_, body) == bodies_.end()) {
bodies_.push_back(body);
}
if (body->angular_damping > 0.0F) {
const float DAMP = std::exp(-body->angular_damping * dt);
body->angular_velocity *= DAMP;
}
// Actualizar posición y rotación
body->position += body->velocity * dt;
body->angle += body->angular_velocity * dt;
body->clearAccumulators();
}
}
// Rebote contra los 4 bordes del rectángulo bounds_.
// Refleja la componente normal de la velocidad por la restitución.
void PhysicsWorld::resolveBoundsCollisions() {
const float MIN_X = bounds_.x;
const float MAX_X = bounds_.x + bounds_.w;
const float MIN_Y = bounds_.y;
const float MAX_Y = bounds_.y + bounds_.h;
void PhysicsWorld::removeBody(RigidBody* body) {
std::erase(bodies_, body);
}
for (auto* body : bodies_) {
if (body == nullptr || body->isStatic()) {
continue;
void PhysicsWorld::update(float dt) {
integrate(dt);
if (has_bounds_) {
resolveBoundsCollisions();
}
const float R = body->radius;
resolveBodyCollisions();
}
// Pared izquierda
if (body->position.x - R < MIN_X) {
body->position.x = MIN_X + R;
if (body->velocity.x < 0.0F) {
body->velocity.x = -body->velocity.x * body->restitution;
// Integración semi-implícita de Euler:
// v(t+dt) = v(t) + (F/m) * dt
// x(t+dt) = x(t) + v(t+dt) * dt
// Más estable que Euler explícito para juegos. Damping exponencial.
void PhysicsWorld::integrate(float dt) {
for (auto* body : bodies_) {
if (body == nullptr || body->isStatic()) {
continue;
}
}
// Pared derecha
if (body->position.x + R > MAX_X) {
body->position.x = MAX_X - R;
if (body->velocity.x > 0.0F) {
body->velocity.x = -body->velocity.x * body->restitution;
// Aplicar fuerzas acumuladas → aceleración
const Vec2 ACCELERATION = body->force_accumulator * body->inverse_mass;
body->velocity += ACCELERATION * dt;
// Damping exponencial: equivalente a v *= exp(-damping * dt)
// Aproximación lineal cuando damping*dt es pequeño.
if (body->linear_damping > 0.0F) {
const float DAMP = std::exp(-body->linear_damping * dt);
body->velocity *= DAMP;
}
}
// Pared superior
if (body->position.y - R < MIN_Y) {
body->position.y = MIN_Y + R;
if (body->velocity.y < 0.0F) {
body->velocity.y = -body->velocity.y * body->restitution;
if (body->angular_damping > 0.0F) {
const float DAMP = std::exp(-body->angular_damping * dt);
body->angular_velocity *= DAMP;
}
// Actualizar posición y rotación
body->position += body->velocity * dt;
body->angle += body->angular_velocity * dt;
body->clearAccumulators();
}
// Pared inferior
if (body->position.y + R > MAX_Y) {
body->position.y = MAX_Y - R;
if (body->velocity.y > 0.0F) {
body->velocity.y = -body->velocity.y * body->restitution;
}
// Rebote contra los 4 bordes del rectángulo bounds_.
// Refleja la componente normal de la velocidad por la restitución.
void PhysicsWorld::resolveBoundsCollisions() {
const float MIN_X = bounds_.x;
const float MAX_X = bounds_.x + bounds_.w;
const float MIN_Y = bounds_.y;
const float MAX_Y = bounds_.y + bounds_.h;
for (auto* body : bodies_) {
if (body == nullptr || body->isStatic()) {
continue;
}
const float R = body->radius;
// Pared izquierda
if (body->position.x - R < MIN_X) {
body->position.x = MIN_X + R;
if (body->velocity.x < 0.0F) {
body->velocity.x = -body->velocity.x * body->restitution;
}
}
// Pared derecha
if (body->position.x + R > MAX_X) {
body->position.x = MAX_X - R;
if (body->velocity.x > 0.0F) {
body->velocity.x = -body->velocity.x * body->restitution;
}
}
// Pared superior
if (body->position.y - R < MIN_Y) {
body->position.y = MIN_Y + R;
if (body->velocity.y < 0.0F) {
body->velocity.y = -body->velocity.y * body->restitution;
}
}
// Pared inferior
if (body->position.y + R > MAX_Y) {
body->position.y = MAX_Y - R;
if (body->velocity.y > 0.0F) {
body->velocity.y = -body->velocity.y * body->restitution;
}
}
}
}
}
// Colisiones cuerpo-cuerpo: O(n²) círculo-círculo + resolución por impulso.
// Para 15 enemigos + 6 balas + 2 naves = ~23 cuerpos → 253 pares. Sobra.
//
// Fórmula del impulso elástico (referencia: Chris Hecker / Box2D):
// j = -(1 + e) * (v_rel · n) / (1/m_a + 1/m_b)
// donde n es la normal del contacto (de a hacia b) y v_rel = v_a - v_b.
void PhysicsWorld::resolveBodyCollisions() {
const std::size_t COUNT = bodies_.size();
for (std::size_t i = 0; i < COUNT; ++i) {
for (std::size_t j = i + 1; j < COUNT; ++j) {
auto* a = bodies_[i];
auto* b = bodies_[j];
if (a != nullptr && b != nullptr) {
resolveBodyPair(*a, *b);
// Colisiones cuerpo-cuerpo: O(n²) círculo-círculo + resolución por impulso.
// Para 15 enemigos + 6 balas + 2 naves = ~23 cuerpos → 253 pares. Sobra.
//
// Fórmula del impulso elástico (referencia: Chris Hecker / Box2D):
// j = -(1 + e) * (v_rel · n) / (1/m_a + 1/m_b)
// donde n es la normal del contacto (de a hacia b) y v_rel = v_a - v_b.
void PhysicsWorld::resolveBodyCollisions() {
const std::size_t COUNT = bodies_.size();
for (std::size_t i = 0; i < COUNT; ++i) {
for (std::size_t j = i + 1; j < COUNT; ++j) {
auto* a = bodies_[i];
auto* b = bodies_[j];
if (a != nullptr && b != nullptr) {
resolveBodyPair(*a, *b);
}
}
}
}
}
void PhysicsWorld::resolveBodyPair(RigidBody& a, RigidBody& b) {
// Dos cuerpos estáticos no necesitan resolución
if (a.isStatic() && b.isStatic()) {
return;
}
void PhysicsWorld::resolveBodyPair(RigidBody& a, RigidBody& b) {
// Dos cuerpos estáticos no necesitan resolución
if (a.isStatic() && b.isStatic()) {
return;
}
const Vec2 DELTA = b.position - a.position;
const float DIST_SQ = DELTA.lengthSquared();
const float SUM_R = a.radius + b.radius;
if (DIST_SQ > SUM_R * SUM_R || DIST_SQ <= 0.0F) {
return;
}
// Un cuerpo con radius=0 es cinemático puro (ej. la bala) y no participa
// en body-body. La detecció de gameplay (Physics::checkCollision) usa
// el radius de l'entity (no el del body) i s'encarrega d'aquesta parella.
if (a.radius <= 0.0F || b.radius <= 0.0F) {
return;
}
const float DIST = std::sqrt(DIST_SQ);
const Vec2 NORMAL = DELTA / DIST; // de A hacia B
const Vec2 DELTA = b.position - a.position;
const float DIST_SQ = DELTA.lengthSquared();
const float SUM_R = a.radius + b.radius;
if (DIST_SQ > SUM_R * SUM_R || DIST_SQ <= 0.0F) {
return;
}
const float DIST = std::sqrt(DIST_SQ);
const Vec2 NORMAL = DELTA / DIST; // de A hacia B
// Corrección posicional (resolver penetración)
const float PENETRATION = SUM_R - DIST;
const float TOTAL_INV_MASS = a.inverse_mass + b.inverse_mass;
if (TOTAL_INV_MASS > 0.0F) {
const Vec2 CORRECTION = NORMAL * (PENETRATION / TOTAL_INV_MASS);
if (!a.isStatic()) {
a.position -= CORRECTION * a.inverse_mass;
}
if (!b.isStatic()) {
b.position += CORRECTION * b.inverse_mass;
}
}
// Velocidad relativa proyectada sobre la normal
const Vec2 V_REL = b.velocity - a.velocity;
const float VEL_ALONG_NORMAL = V_REL.dot(NORMAL);
// Si se están separando, no aplicar impulso
if (VEL_ALONG_NORMAL > 0.0F) {
return;
}
// Restitución promedio (Box2D usa max; promedio es más permisivo)
const float E = (a.restitution + b.restitution) * 0.5F;
const float J = -(1.0F + E) * VEL_ALONG_NORMAL / TOTAL_INV_MASS;
const Vec2 IMPULSE = NORMAL * J;
// Corrección posicional (resolver penetración)
const float PENETRATION = SUM_R - DIST;
const float TOTAL_INV_MASS = a.inverse_mass + b.inverse_mass;
if (TOTAL_INV_MASS > 0.0F) {
const Vec2 CORRECTION = NORMAL * (PENETRATION / TOTAL_INV_MASS);
if (!a.isStatic()) {
a.position -= CORRECTION * a.inverse_mass;
a.velocity -= IMPULSE * a.inverse_mass;
}
if (!b.isStatic()) {
b.position += CORRECTION * b.inverse_mass;
b.velocity += IMPULSE * b.inverse_mass;
}
}
// Velocidad relativa proyectada sobre la normal
const Vec2 V_REL = b.velocity - a.velocity;
const float VEL_ALONG_NORMAL = V_REL.dot(NORMAL);
// Si se están separando, no aplicar impulso
if (VEL_ALONG_NORMAL > 0.0F) {
return;
}
// Restitución promedio (Box2D usa max; promedio es más permisivo)
const float E = (a.restitution + b.restitution) * 0.5F;
const float J = -(1.0F + E) * VEL_ALONG_NORMAL / TOTAL_INV_MASS;
const Vec2 IMPULSE = NORMAL * J;
if (!a.isStatic()) {
a.velocity -= IMPULSE * a.inverse_mass;
}
if (!b.isStatic()) {
b.velocity += IMPULSE * b.inverse_mass;
}
}
} // namespace Physics
+38 -85
View File
@@ -1,105 +1,58 @@
// gpu_device.cpp - Implementación del wrapper de SDL_GPUDevice
// gpu_device.cpp - Implementació del wrapper de SDL_GPUDevice
#include "core/rendering/gpu/gpu_device.hpp"
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include "core/utils/path_utils.hpp"
namespace Rendering::GPU {
GpuDevice::~GpuDevice() { destroy(); }
GpuDevice::~GpuDevice() { destroy(); }
auto GpuDevice::init(SDL_Window* window) -> bool {
window_ = window;
auto GpuDevice::init(SDL_Window* window) -> bool {
window_ = window;
// Solicitar backends en orden de preferencia: Vulkan (Linux/Windows),
// Metal (macOS). Sin DirectX según decisión de proyecto.
// SDL_GPU_SHADERFORMAT_SPIRV: shaders compilados con glslc.
// SDL_GPU_SHADERFORMAT_MSL: pendiente para macOS (Fase futura).
constexpr SDL_GPUShaderFormat SHADER_FORMATS =
SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL;
// Selecció del format de shader per plataforma:
// - macOS → MSL (Metal Shading Language), shaders embedits a gpu/msl/*.msl.h
// - Resta → SPIR-V (Vulkan), shaders embedits a gpu/spv/*.h
#ifdef __APPLE__
constexpr SDL_GPUShaderFormat SHADER_FORMATS = SDL_GPU_SHADERFORMAT_MSL;
#else
constexpr SDL_GPUShaderFormat SHADER_FORMATS = SDL_GPU_SHADERFORMAT_SPIRV;
#endif
device_ = SDL_CreateGPUDevice(SHADER_FORMATS, /*debug=*/true, /*name=*/nullptr);
if (device_ == nullptr) {
std::cerr << "[GpuDevice] SDL_CreateGPUDevice falló: " << SDL_GetError() << '\n';
return false;
}
const char* driver = SDL_GetGPUDeviceDriver(device_);
std::cout << "[GpuDevice] Backend GPU: " << (driver != nullptr ? driver : "?") << '\n';
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
std::cerr << "[GpuDevice] SDL_ClaimWindowForGPUDevice falló: " << SDL_GetError() << '\n';
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
return false;
}
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
return true;
}
void GpuDevice::destroy() {
if (device_ != nullptr) {
if (window_ != nullptr) {
SDL_ReleaseWindowFromGPUDevice(device_, window_);
device_ = SDL_CreateGPUDevice(SHADER_FORMATS, /*debug_mode=*/true, /*name=*/nullptr);
if (device_ == nullptr) {
std::cerr << "[GpuDevice] SDL_CreateGPUDevice falló: " << SDL_GetError() << '\n';
return false;
}
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
}
window_ = nullptr;
swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
}
auto GpuDevice::loadShader(const std::string& spv_filename,
SDL_GPUShaderStage stage,
uint32_t num_uniform_buffers,
uint32_t num_samplers) const -> SDL_GPUShader* {
if (device_ == nullptr) {
return nullptr;
const char* driver = SDL_GetGPUDeviceDriver(device_);
std::cout << "[GpuDevice] Backend GPU: " << (driver != nullptr ? driver : "?") << '\n';
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
std::cerr << "[GpuDevice] SDL_ClaimWindowForGPUDevice falló: " << SDL_GetError() << '\n';
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
return false;
}
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
return true;
}
// Los .spv viven en build/shaders/ junto al binario.
const std::string PATH = Utils::getExecutableDirectory() + "/shaders/" + spv_filename;
std::ifstream file(PATH, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
std::cerr << "[GpuDevice] No s'ha pogut obrir el shader: " << PATH << '\n';
return nullptr;
void GpuDevice::destroy() {
if (device_ != nullptr) {
if (window_ != nullptr) {
SDL_ReleaseWindowFromGPUDevice(device_, window_);
}
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
}
window_ = nullptr;
swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
}
const std::streamsize SIZE = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(static_cast<size_t>(SIZE));
if (!file.read(reinterpret_cast<char*>(buffer.data()), SIZE)) {
std::cerr << "[GpuDevice] Error llegint shader: " << PATH << '\n';
return nullptr;
}
SDL_GPUShaderCreateInfo info{};
info.code = buffer.data();
info.code_size = buffer.size();
info.entrypoint = "main";
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
info.stage = stage;
info.num_uniform_buffers = num_uniform_buffers;
info.num_samplers = num_samplers;
info.num_storage_buffers = 0;
info.num_storage_textures = 0;
SDL_GPUShader* shader = SDL_CreateGPUShader(device_, &info);
if (shader == nullptr) {
std::cerr << "[GpuDevice] SDL_CreateGPUShader (" << spv_filename << "): " << SDL_GetError() << '\n';
}
return shader;
}
} // namespace Rendering::GPU
+16 -24
View File
@@ -1,13 +1,17 @@
// gpu_device.hpp - Wrapper de SDL_GPUDevice
// © 2026 JailDesigner
//
// Ownership del SDL_GPUDevice y del claim del window. Backend preferido:
// Vulkan (Linux, Windows) y Metal (macOS). Sin DirectX.
// Ownership del SDL_GPUDevice i del claim del window. Backend per plataforma:
// Vulkan (Linux, Windows) i Metal (macOS). Sense DirectX.
//
// Uso:
// Els shaders s'embedien al binari (no es carreguen de disc): SPIR-V via
// headers generats per CMake a `gpu/spv/*.h`, i MSL a mà a `gpu/msl/*.msl.h`.
// La creació dels SDL_GPUShader la fan els pipelines via shader_factory.hpp.
//
// Ús:
// GpuDevice device;
// if (!device.init(window)) return -1; // claim del window
// ... renderer setup ...
// ... pipelines setup ...
// device.destroy(); // unclaim + destroy device
#pragma once
@@ -15,45 +19,33 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <string>
namespace Rendering::GPU {
class GpuDevice {
public:
class GpuDevice {
public:
GpuDevice() = default;
~GpuDevice();
// No copia / move (RAII propietario del device).
// No copia / move (RAII propietari del device).
GpuDevice(const GpuDevice&) = delete;
auto operator=(const GpuDevice&) -> GpuDevice& = delete;
GpuDevice(GpuDevice&&) = delete;
auto operator=(GpuDevice&&) -> GpuDevice& = delete;
// Crea el device y claim el window. Devuelve false si no hay backend
// soportado o si el driver no permite el claim.
// Crea el device i claim el window. Selecciona MSL en macOS, SPIR-V
// en la resta. Retorna false si no hi ha backend suportat o si el
// driver no permet el claim.
[[nodiscard]] auto init(SDL_Window* window) -> bool;
void destroy();
// Carga un shader SPIR-V desde build/shaders/{name}.spv. Devuelve un
// SDL_GPUShader* del que ahora es responsable el caller (libera con
// SDL_ReleaseGPUShader). Retorna nullptr si falla.
//
// num_uniform_buffers: nº de uniform buffers que usa el shader (slot 0..N-1).
// num_samplers: nº de samplers (combined image+sampler) usados por el shader.
[[nodiscard]] auto loadShader(const std::string& spv_filename,
SDL_GPUShaderStage stage,
uint32_t num_uniform_buffers,
uint32_t num_samplers = 0) const -> SDL_GPUShader*;
[[nodiscard]] auto get() const -> SDL_GPUDevice* { return device_; }
[[nodiscard]] auto window() const -> SDL_Window* { return window_; }
[[nodiscard]] auto swapchainFormat() const -> SDL_GPUTextureFormat { return swapchain_format_; }
private:
private:
SDL_GPUDevice* device_{nullptr};
SDL_Window* window_{nullptr};
SDL_GPUTextureFormat swapchain_format_{SDL_GPU_TEXTUREFORMAT_INVALID};
};
};
} // namespace Rendering::GPU
@@ -14,9 +14,11 @@ namespace Rendering::GPU {
GpuFrameRenderer::~GpuFrameRenderer() { destroy(); }
auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h) -> bool {
auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h, float render_w, float render_h) -> bool {
logical_w_ = logical_w;
logical_h_ = logical_h;
render_w_ = render_w;
render_h_ = render_h;
if (!device_.init(window)) {
return false;
@@ -47,13 +49,15 @@ namespace Rendering::GPU {
return false;
}
// Textura offscreen del tamaño lógico del juego, COLOR_TARGET + SAMPLER.
// Textura offscreen del tamaño físico de render, COLOR_TARGET + SAMPLER.
// El tamaño lógico se aplica a los vértices vía UBO; el offscreen puede
// ser de mayor resolución para ganar nitidez tras el upscale a la swapchain.
SDL_GPUTextureCreateInfo tex_info{};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = offscreen_format_;
tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
tex_info.width = static_cast<uint32_t>(logical_w_);
tex_info.height = static_cast<uint32_t>(logical_h_);
tex_info.width = static_cast<uint32_t>(render_w_);
tex_info.height = static_cast<uint32_t>(render_h_);
tex_info.layer_count_or_depth = 1;
tex_info.num_levels = 1;
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
@@ -107,6 +111,19 @@ namespace Rendering::GPU {
indices_.clear();
}
auto GpuFrameRenderer::resizeRenderTarget(float render_w, float render_h) -> bool {
// Solo seguro fuera de un frame: si el cmd buffer está vivo y referencia
// la textura antigua, recrearla provocaría un cuelgue/UB.
if (isInsideFrame()) {
std::cerr << "[GpuFrameRenderer] resizeRenderTarget llamado dentro de frame, ignorado\n";
return false;
}
destroyOffscreen();
render_w_ = render_w;
render_h_ = render_h;
return createOffscreen();
}
auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) -> bool {
// Los clear_* se ignoran: el fondo lo pinta el postpro. Mantenemos la
// firma para no romper el SDLManager.
@@ -438,8 +455,9 @@ namespace Rendering::GPU {
ubo.background_max_g = BG_MAX_G;
ubo.background_max_b = BG_MAX_B;
ubo.background_max_a = 1.0F;
ubo.texel_size_x = 1.0F / logical_w_;
ubo.texel_size_y = 1.0F / logical_h_;
// El sampling del bloom muestrea el offscreen → texel size del tamaño físico.
ubo.texel_size_x = 1.0F / render_w_;
ubo.texel_size_y = 1.0F / render_h_;
ubo.pad_b = 0.0F;
ubo.pad_c = 0.0F;
@@ -59,12 +59,24 @@ namespace Rendering::GPU {
GpuFrameRenderer(GpuFrameRenderer&&) = delete;
auto operator=(GpuFrameRenderer&&) -> GpuFrameRenderer& = delete;
// Crea device + pipeline + offscreen + sampler. logical_w/h = tamaño
// en píxeles lógicos del juego (1280×720), usado como base del
// offscreen y de la transformación a NDC del shader de líneas.
[[nodiscard]] auto init(SDL_Window* window, float logical_w, float logical_h) -> bool;
// Crea device + pipeline + offscreen + sampler.
// logical_w/h: tamaño en píxeles lógicos del juego (1280×720). Lo
// consume el shader de líneas para transformar a NDC.
// render_w/h: tamaño físico del offscreen donde se rasterizan las
// líneas. Puede ser > logical para ganar nitidez al
// escalar a la swapchain (configurable vía YAML).
[[nodiscard]] auto init(SDL_Window* window,
float logical_w,
float logical_h,
float render_w,
float render_h) -> bool;
void destroy();
// Recrea el offscreen con un nuevo tamaño físico de render. Solo es
// seguro fuera de un frame (isInsideFrame() == false). Devuelve false
// si está dentro de frame o si la creación de la textura falla.
[[nodiscard]] auto resizeRenderTarget(float render_w, float render_h) -> bool;
// beginFrame: adquiere swapchain, abre render pass sobre offscreen
// con clear a negro. Devuelve false si no hay textura disponible.
// Los argumentos clear_r/g/b se ignoran (compatibilidad de API: el
@@ -114,10 +126,18 @@ namespace Rendering::GPU {
GpuLinePipeline line_pipeline_;
GpuPostFxPipeline postfx_pipeline_;
// Tamaño lógico del juego (= tamaño del offscreen).
// Tamaño lógico del juego: espacio de coordenadas de las primitivas
// (vértices, UBO del line shader). Fijo a 1280×720.
float logical_w_{1280.0F};
float logical_h_{720.0F};
// Tamaño físico del offscreen (configurable). Independiente del lógico:
// las coordenadas en NDC son agnósticas a la resolución de salida, así
// que rasterizar a mayor render_w_/h_ da líneas más nítidas tras el
// upscale lineal a la swapchain.
float render_w_{1280.0F};
float render_h_{720.0F};
// Viewport del pase final en píxeles físicos. <0 = full window.
float viewport_x_{0.0F};
float viewport_y_{0.0F};
@@ -1,4 +1,4 @@
// gpu_line_pipeline.cpp - Implementación del pipeline de líneas
// gpu_line_pipeline.cpp - Implementació del pipeline de línies
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
@@ -8,6 +8,15 @@
#include <iostream>
#include "core/rendering/gpu/gpu_device.hpp"
#include "core/rendering/gpu/shader_factory.hpp"
#ifdef __APPLE__
#include "core/rendering/gpu/msl/line_frag.msl.h"
#include "core/rendering/gpu/msl/line_vert.msl.h"
#else
#include "core/rendering/gpu/spv/line_frag_spv.h"
#include "core/rendering/gpu/spv/line_vert_spv.h"
#endif
namespace Rendering::GPU {
@@ -20,12 +29,37 @@ namespace Rendering::GPU {
return false;
}
SDL_GPUShader* vert = device.loadShader("line.vert.spv",
// Vertex: 1 UBO (viewport_size). Fragment: cap recurs.
#ifdef __APPLE__
SDL_GPUShader* vert = createShaderMSL(owner_,
Msl::LINE_VERT_MSL,
"line_vs",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_samplers=*/0,
/*num_uniform_buffers=*/1);
SDL_GPUShader* frag = device.loadShader("line.frag.spv",
SDL_GPUShader* frag = createShaderMSL(owner_,
Msl::LINE_FRAG_MSL,
"line_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/0,
/*num_uniform_buffers=*/0);
#else
SDL_GPUShader* vert = createShaderSPIRV(owner_,
LINE_VERT_SPV,
LINE_VERT_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_samplers=*/0,
/*num_uniform_buffers=*/1);
SDL_GPUShader* frag = createShaderSPIRV(owner_,
LINE_FRAG_SPV,
LINE_FRAG_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/0,
/*num_uniform_buffers=*/0);
#endif
if ((vert == nullptr) || (frag == nullptr)) {
if (vert != nullptr) {
SDL_ReleaseGPUShader(owner_, vert);
@@ -33,7 +67,7 @@ namespace Rendering::GPU {
if (frag != nullptr) {
SDL_ReleaseGPUShader(owner_, frag);
}
std::cerr << "[GpuLinePipeline] Error cargando shaders\n";
std::cerr << "[GpuLinePipeline] Error cargando shaders: " << SDL_GetError() << '\n';
return false;
}
@@ -98,8 +132,8 @@ namespace Rendering::GPU {
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
// Los shaders se pueden liberar tras crear el pipeline (SDL los retiene
// internamente mientras el pipeline esté vivo).
// Els shaders es poden alliberar després de crear el pipeline (SDL els
// reté internament mentre el pipeline estigui viu).
SDL_ReleaseGPUShader(owner_, vert);
SDL_ReleaseGPUShader(owner_, frag);
+109 -78
View File
@@ -1,4 +1,4 @@
// gpu_postfx_pipeline.cpp - Implementación del pipeline de postprocesado.
// gpu_postfx_pipeline.cpp - Implementació del pipeline de postprocesat.
#include "core/rendering/gpu/gpu_postfx_pipeline.hpp"
@@ -8,91 +8,122 @@
#include <iostream>
#include "core/rendering/gpu/gpu_device.hpp"
#include "core/rendering/gpu/shader_factory.hpp"
#ifdef __APPLE__
#include "core/rendering/gpu/msl/postfx_frag.msl.h"
#include "core/rendering/gpu/msl/postfx_vert.msl.h"
#else
#include "core/rendering/gpu/spv/postfx_frag_spv.h"
#include "core/rendering/gpu/spv/postfx_vert_spv.h"
#endif
namespace Rendering::GPU {
GpuPostFxPipeline::~GpuPostFxPipeline() { destroy(); }
GpuPostFxPipeline::~GpuPostFxPipeline() { destroy(); }
auto GpuPostFxPipeline::init(const GpuDevice& device,
SDL_GPUTextureFormat target_format) -> bool {
owner_ = device.get();
if (owner_ == nullptr) {
return false;
}
// El vertex shader no usa UBO (emite tres vértices hardcodeados).
// El fragment shader usa 1 sampler (escena) y 1 UBO (parámetros postpro).
SDL_GPUShader* vert = device.loadShader("postfx.vert.spv",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_uniform_buffers=*/0,
/*num_samplers=*/0);
SDL_GPUShader* frag = device.loadShader("postfx.frag.spv",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_uniform_buffers=*/1,
/*num_samplers=*/1);
if ((vert == nullptr) || (frag == nullptr)) {
if (vert != nullptr) {
SDL_ReleaseGPUShader(owner_, vert);
auto GpuPostFxPipeline::init(const GpuDevice& device,
SDL_GPUTextureFormat target_format) -> bool {
owner_ = device.get();
if (owner_ == nullptr) {
return false;
}
if (frag != nullptr) {
SDL_ReleaseGPUShader(owner_, frag);
// Vertex shader: sense UBO ni vertex buffer (emet 3 vèrtexs hardcodejats).
// Fragment shader: 1 sampler (escena) + 1 UBO (paràmetres postpro).
#ifdef __APPLE__
SDL_GPUShader* vert = createShaderMSL(owner_,
Msl::POSTFX_VERT_MSL,
"postfx_vs",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_samplers=*/0,
/*num_uniform_buffers=*/0);
SDL_GPUShader* frag = createShaderMSL(owner_,
Msl::POSTFX_FRAG_MSL,
"postfx_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/1,
/*num_uniform_buffers=*/1);
#else
SDL_GPUShader* vert = createShaderSPIRV(owner_,
POSTFX_VERT_SPV,
POSTFX_VERT_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_samplers=*/0,
/*num_uniform_buffers=*/0);
SDL_GPUShader* frag = createShaderSPIRV(owner_,
POSTFX_FRAG_SPV,
POSTFX_FRAG_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/1,
/*num_uniform_buffers=*/1);
#endif
if ((vert == nullptr) || (frag == nullptr)) {
if (vert != nullptr) {
SDL_ReleaseGPUShader(owner_, vert);
}
if (frag != nullptr) {
SDL_ReleaseGPUShader(owner_, frag);
}
std::cerr << "[GpuPostFxPipeline] Error cargando shaders postfx: " << SDL_GetError() << '\n';
return false;
}
std::cerr << "[GpuPostFxPipeline] Error cargando shaders postfx\n";
return false;
// Sense vertex input: els tres vèrtexs del triangle es generen al shader.
SDL_GPUVertexInputState vertex_input{};
vertex_input.vertex_buffer_descriptions = nullptr;
vertex_input.num_vertex_buffers = 0;
vertex_input.vertex_attributes = nullptr;
vertex_input.num_vertex_attributes = 0;
// Color target del postpro = swapchain. Sense blending: el postpro reescriu
// píxels directament (la mescla amb l'escena ja es fa dins del shader).
SDL_GPUColorTargetDescription color_target{};
color_target.format = target_format;
color_target.blend_state.enable_blend = false;
color_target.blend_state.color_write_mask =
SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G |
SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A;
SDL_GPUGraphicsPipelineTargetInfo target_info{};
target_info.color_target_descriptions = &color_target;
target_info.num_color_targets = 1;
target_info.has_depth_stencil_target = false;
SDL_GPUGraphicsPipelineCreateInfo info{};
info.vertex_shader = vert;
info.fragment_shader = frag;
info.vertex_input_state = vertex_input;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
info.depth_stencil_state = {};
info.target_info = target_info;
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
SDL_ReleaseGPUShader(owner_, vert);
SDL_ReleaseGPUShader(owner_, frag);
if (pipeline_ == nullptr) {
std::cerr << "[GpuPostFxPipeline] SDL_CreateGPUGraphicsPipeline: "
<< SDL_GetError() << '\n';
return false;
}
return true;
}
// Sin vertex input: los tres vértices del triángulo se generan en el shader.
SDL_GPUVertexInputState vertex_input{};
vertex_input.vertex_buffer_descriptions = nullptr;
vertex_input.num_vertex_buffers = 0;
vertex_input.vertex_attributes = nullptr;
vertex_input.num_vertex_attributes = 0;
// Color target del postpro = swapchain. Sin blending: el postpro reescribe
// píxeles directamente (la mezcla con la escena ya se hizo dentro del shader).
SDL_GPUColorTargetDescription color_target{};
color_target.format = target_format;
color_target.blend_state.enable_blend = false;
color_target.blend_state.color_write_mask =
SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G |
SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A;
SDL_GPUGraphicsPipelineTargetInfo target_info{};
target_info.color_target_descriptions = &color_target;
target_info.num_color_targets = 1;
target_info.has_depth_stencil_target = false;
SDL_GPUGraphicsPipelineCreateInfo info{};
info.vertex_shader = vert;
info.fragment_shader = frag;
info.vertex_input_state = vertex_input;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
info.depth_stencil_state = {};
info.target_info = target_info;
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
SDL_ReleaseGPUShader(owner_, vert);
SDL_ReleaseGPUShader(owner_, frag);
if (pipeline_ == nullptr) {
std::cerr << "[GpuPostFxPipeline] SDL_CreateGPUGraphicsPipeline: "
<< SDL_GetError() << '\n';
return false;
void GpuPostFxPipeline::destroy() {
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
}
pipeline_ = nullptr;
owner_ = nullptr;
}
return true;
}
void GpuPostFxPipeline::destroy() {
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
}
pipeline_ = nullptr;
owner_ = nullptr;
}
} // namespace Rendering::GPU
@@ -0,0 +1,34 @@
// line_frag.msl.h - Metal Shading Language del fragment shader de línies
// © 2026 JailDesigner
//
// IMPORTANT: mantenir sincronitzat a mà amb shaders/line.frag.glsl. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic.
//
// Aplica antialias geomètric via smoothstep sobre edge_dist (±1 als laterals,
// 0 al centre del quad). Sense uniforms ni samplers.
#pragma once
#ifdef __APPLE__
namespace Rendering::GPU::Msl {
inline constexpr const char* LINE_FRAG_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct VOut {
float4 pos [[position]];
float4 color;
float edge_dist;
};
fragment float4 line_fs(VOut in [[stage_in]]) {
float d = abs(in.edge_dist);
float alpha = 1.0 - smoothstep(0.7, 1.0, d);
return float4(in.color.rgb, in.color.a * alpha);
}
)";
} // namespace Rendering::GPU::Msl
#endif // __APPLE__
@@ -0,0 +1,54 @@
// line_vert.msl.h - Metal Shading Language del vertex shader de línies
// © 2026 JailDesigner
//
// IMPORTANT: mantenir sincronitzat a mà amb shaders/line.vert.glsl. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
// canvi al UBO o al layout de vèrtex cal replicar-lo ací al mateix commit.
//
// Mapping SDL3 GPU → Metal (segons src/gpu/metal/SDL_gpu_metal.m upstream):
// - Vertex buffers d'usuari: [[buffer(14 + slot)]] (METAL_FIRST_VERTEX_BUFFER_SLOT=14).
// A través de [[stage_in]] amb [[attribute(N)]], Metal resol automàticament
// a partir del MTLVertexDescriptor del pipeline state (transparent al MSL).
// - Vertex UBO slot 0 SDL → [[buffer(0)]] MSL (sense offset).
#pragma once
#ifdef __APPLE__
namespace Rendering::GPU::Msl {
inline constexpr const char* LINE_VERT_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct VIn {
float2 in_position [[attribute(0)]];
float4 in_color [[attribute(1)]];
float in_edge_dist[[attribute(2)]];
};
struct VOut {
float4 pos [[position]];
float4 color;
float edge_dist;
};
struct LineUBO {
float2 viewport_size;
float2 _padding;
};
vertex VOut line_vs(VIn in [[stage_in]],
constant LineUBO& ubo [[buffer(0)]]) {
float2 ndc = (in.in_position / ubo.viewport_size) * 2.0 - 1.0;
ndc.y = -ndc.y;
VOut out;
out.pos = float4(ndc, 0.0, 1.0);
out.color = in.in_color;
out.edge_dist = in.in_edge_dist;
return out;
}
)";
} // namespace Rendering::GPU::Msl
#endif // __APPLE__
@@ -0,0 +1,90 @@
// postfx_frag.msl.h - Metal Shading Language del fragment shader del postpro
// © 2026 JailDesigner
//
// IMPORTANT: mantenir sincronitzat a mà amb shaders/postfx.frag.glsl. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
// canvi al struct PostFxUniforms (gpu_postfx_pipeline.hpp), al GLSL o al MSL
// cal replicar-lo a totes tres al mateix commit.
//
// Composició final: bloom 5×5 amb high-pass, flicker sinusoidal global,
// background pulse sumat. Recursos:
// - texture2d<float> scene [[texture(0)]] + sampler [[sampler(0)]]
// - constant PostFxUBO& ubo [[buffer(0)]] (slot 0 SDL → buffer(0) MSL)
//
// L'struct PostFxUBO té layout idèntic a PostFxUniforms (5×vec4 = 80 bytes).
#pragma once
#ifdef __APPLE__
namespace Rendering::GPU::Msl {
inline constexpr const char* POSTFX_FRAG_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct PostFxUBO {
float time;
float bloom_intensity;
float bloom_threshold;
float bloom_radius_px;
float flicker_amplitude;
float flicker_frequency_hz;
float background_pulse_freq_hz;
float pad_a;
float4 background_min;
float4 background_max;
float2 texel_size;
float2 pad_b;
};
constant float TAU = 6.28318530718;
fragment float4 postfx_fs(PostVOut in [[stage_in]],
texture2d<float> scene [[texture(0)]],
sampler samp [[sampler(0)]],
constant PostFxUBO& ubo [[buffer(0)]]) {
// === BLOOM ===
float3 src = scene.sample(samp, in.uv).rgb;
float3 bloom = float3(0.0);
float total_weight = 0.0;
for (int dy = -2; dy <= 2; ++dy) {
for (int dx = -2; dx <= 2; ++dx) {
float2 offset = float2(float(dx), float(dy)) * ubo.texel_size * ubo.bloom_radius_px;
float3 c = scene.sample(samp, in.uv + offset).rgb;
float luma = max(c.r, max(c.g, c.b));
float high_pass = max(0.0, luma - ubo.bloom_threshold);
float w = exp(-float(dx * dx + dy * dy) / 4.0);
bloom += c * high_pass * w;
total_weight += w;
}
}
if (total_weight > 0.0) {
bloom /= total_weight;
}
bloom *= ubo.bloom_intensity;
// === FLICKER ===
float pulse = (sin(ubo.time * ubo.flicker_frequency_hz * TAU) * 0.5) + 0.5;
float flicker = 1.0 - (ubo.flicker_amplitude * (1.0 - pulse));
// === BACKGROUND PULSE ===
float bg_pulse = (sin(ubo.time * ubo.background_pulse_freq_hz * TAU) * 0.5) + 0.5;
float3 background = mix(ubo.background_min.rgb, ubo.background_max.rgb, bg_pulse);
// === COMPOSICIÓ ===
float3 lines_and_glow = (src + bloom) * flicker;
return float4(background + lines_and_glow, 1.0);
}
)";
} // namespace Rendering::GPU::Msl
#endif // __APPLE__
@@ -0,0 +1,38 @@
// postfx_vert.msl.h - Metal Shading Language del vertex shader del postpro
// © 2026 JailDesigner
//
// IMPORTANT: mantenir sincronitzat a mà amb shaders/postfx.vert.glsl. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic.
//
// Fullscreen triangle: 3 vèrtexs en (-1,-1), (3,-1), (-1,3) generats per
// [[vertex_id]]. UV.y invertida per compensar la diferència de convenció
// entre clip-space del line shader (Y-flip) i el mostreig SDL_gpu (origen
// top-left). Sense uniforms ni vertex buffers (DrawPrimitives vertex_count=3).
#pragma once
#ifdef __APPLE__
namespace Rendering::GPU::Msl {
inline constexpr const char* POSTFX_VERT_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
const float2 positions[3] = { {-1.0, -1.0}, { 3.0, -1.0}, {-1.0, 3.0} };
const float2 uvs[3] = { { 0.0, 1.0}, { 2.0, 1.0}, { 0.0, -1.0} };
PostVOut out;
out.pos = float4(positions[vid], 0.0, 1.0);
out.uv = uvs[vid];
return out;
}
)";
} // namespace Rendering::GPU::Msl
#endif // __APPLE__
@@ -0,0 +1,60 @@
// shader_factory.hpp - Helpers per crear SDL_GPUShader segons plataforma
// © 2026 JailDesigner
//
// En __APPLE__ s'utilitza MSL (Metal Shading Language) embedit com a string
// literal C++. En la resta de plataformes (Linux/Windows) s'utilitza SPIR-V
// embedit com a uint8_t[] en headers generats per CMake. La selecció és
// compile-time via #ifdef.
#pragma once
#include <SDL3/SDL_gpu.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace Rendering::GPU {
#ifdef __APPLE__
inline auto createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info{};
info.code = reinterpret_cast<const Uint8*>(msl_source);
info.code_size = std::strlen(msl_source) + 1;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_MSL;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_uniform_buffers = num_uniform_buffers;
info.num_storage_buffers = 0;
info.num_storage_textures = 0;
return SDL_CreateGPUShader(device, &info);
}
#else
inline auto createShaderSPIRV(SDL_GPUDevice* device,
const std::uint8_t* spv_code,
std::size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info{};
info.code = spv_code;
info.code_size = spv_size;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_uniform_buffers = num_uniform_buffers;
info.num_storage_buffers = 0;
info.num_storage_textures = 0;
return SDL_CreateGPUShader(device, &info);
}
#endif
} // namespace Rendering::GPU
@@ -0,0 +1,670 @@
#pragma once
#include <cstddef>
#include <cstdint>
static const uint8_t LINE_FRAG_SPV[] = {
0x03,
0x02,
0x23,
0x07,
0x00,
0x00,
0x01,
0x00,
0x0b,
0x00,
0x0d,
0x00,
0x25,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x11,
0x00,
0x02,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x06,
0x00,
0x01,
0x00,
0x00,
0x00,
0x47,
0x4c,
0x53,
0x4c,
0x2e,
0x73,
0x74,
0x64,
0x2e,
0x34,
0x35,
0x30,
0x00,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x03,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x08,
0x00,
0x04,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x6d,
0x61,
0x69,
0x6e,
0x00,
0x00,
0x00,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x15,
0x00,
0x00,
0x00,
0x17,
0x00,
0x00,
0x00,
0x10,
0x00,
0x03,
0x00,
0x04,
0x00,
0x00,
0x00,
0x07,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x15,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x17,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x13,
0x00,
0x02,
0x00,
0x02,
0x00,
0x00,
0x00,
0x21,
0x00,
0x03,
0x00,
0x03,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x16,
0x00,
0x03,
0x00,
0x06,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x09,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x09,
0x00,
0x00,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x2b,
0x00,
0x04,
0x00,
0x06,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x80,
0x3f,
0x2b,
0x00,
0x04,
0x00,
0x06,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x33,
0x33,
0x33,
0x3f,
0x17,
0x00,
0x04,
0x00,
0x13,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x14,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x14,
0x00,
0x00,
0x00,
0x15,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x16,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x16,
0x00,
0x00,
0x00,
0x17,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x15,
0x00,
0x04,
0x00,
0x1b,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x2b,
0x00,
0x04,
0x00,
0x1b,
0x00,
0x00,
0x00,
0x1c,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x36,
0x00,
0x05,
0x00,
0x02,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0xf8,
0x00,
0x02,
0x00,
0x05,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x06,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x0c,
0x00,
0x06,
0x00,
0x06,
0x00,
0x00,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0c,
0x00,
0x08,
0x00,
0x06,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x31,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x83,
0x00,
0x05,
0x00,
0x06,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x13,
0x00,
0x00,
0x00,
0x19,
0x00,
0x00,
0x00,
0x17,
0x00,
0x00,
0x00,
0x41,
0x00,
0x05,
0x00,
0x09,
0x00,
0x00,
0x00,
0x1d,
0x00,
0x00,
0x00,
0x17,
0x00,
0x00,
0x00,
0x1c,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x06,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x1d,
0x00,
0x00,
0x00,
0x85,
0x00,
0x05,
0x00,
0x06,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x51,
0x00,
0x05,
0x00,
0x06,
0x00,
0x00,
0x00,
0x21,
0x00,
0x00,
0x00,
0x19,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x51,
0x00,
0x05,
0x00,
0x06,
0x00,
0x00,
0x00,
0x22,
0x00,
0x00,
0x00,
0x19,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x51,
0x00,
0x05,
0x00,
0x06,
0x00,
0x00,
0x00,
0x23,
0x00,
0x00,
0x00,
0x19,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x50,
0x00,
0x07,
0x00,
0x13,
0x00,
0x00,
0x00,
0x24,
0x00,
0x00,
0x00,
0x21,
0x00,
0x00,
0x00,
0x22,
0x00,
0x00,
0x00,
0x23,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x3e,
0x00,
0x03,
0x00,
0x15,
0x00,
0x00,
0x00,
0x24,
0x00,
0x00,
0x00,
0xfd,
0x00,
0x01,
0x00,
0x38,
0x00,
0x01,
0x00,
};
static const size_t LINE_FRAG_SPV_SIZE = 664;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+42 -38
View File
@@ -3,52 +3,56 @@
#include "core/rendering/line_renderer.hpp"
#include "core/defaults.hpp"
namespace Rendering {
// Color global compartido para líneas sin paleta propia (HUD, debug, texto
// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro.
SDL_Color g_current_line_color = {100, 255, 100, 255};
// Color global compartido para líneas sin paleta propia (HUD, debug, texto
// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro.
SDL_Color g_current_line_color = {100, 255, 100, 255};
// Grosor global por defecto. Configurable via setLineThickness.
// 1.5 da una línea visible y crujiente; 1.0 se ve demasiado fino en pantallas grandes.
float g_current_line_thickness = 1.5F;
// Grosor global por defecto. Configurable via setLineThickness.
float g_current_line_thickness = Defaults::Rendering::LINE_THICKNESS_DEFAULT;
void linea(Renderer* renderer,
int x1, int y1, int x2, int y2,
float brightness,
float thickness,
SDL_Color color) {
if (renderer == nullptr) {
return;
void linea(Renderer* renderer,
int x1,
int y1,
int x2,
int y2,
float brightness,
float thickness,
SDL_Color color) {
if (renderer == nullptr) {
return;
}
// Coords lógicas (1280×720). El shader hace el mapeo a NDC; el viewport
// del SDLManager hace el letterbox a píxeles físicos.
const auto FX1 = static_cast<float>(x1);
const auto FY1 = static_cast<float>(y1);
const auto FX2 = static_cast<float>(x2);
const auto FY2 = static_cast<float>(y2);
// color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo.
const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color;
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
const float W = (thickness > 0.0F) ? thickness : g_current_line_thickness;
renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, 1.0F);
}
// Coords lógicas (1280×720). El shader hace el mapeo a NDC; el viewport
// del SDLManager hace el letterbox a píxeles físicos.
const auto FX1 = static_cast<float>(x1);
const auto FY1 = static_cast<float>(y1);
const auto FX2 = static_cast<float>(x2);
const auto FY2 = static_cast<float>(y2);
void setLineColor(SDL_Color color) { g_current_line_color = color; }
// color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo.
const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color;
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
const float W = (thickness > 0.0F) ? thickness : g_current_line_thickness;
renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, 1.0F);
}
void setLineColor(SDL_Color color) { g_current_line_color = color; }
void setLineThickness(float thickness) {
if (thickness > 0.0F) {
g_current_line_thickness = thickness;
void setLineThickness(float thickness) {
if (thickness > 0.0F) {
g_current_line_thickness = thickness;
}
}
}
auto getLineThickness() -> float { return g_current_line_thickness; }
auto getLineThickness() -> float { return g_current_line_thickness; }
} // namespace Rendering
+38 -5
View File
@@ -10,7 +10,9 @@
#include <iostream>
#include "core/config/postfx_config.hpp"
#include "core/defaults.hpp"
#include "core/defaults/game.hpp"
#include "core/defaults/rendering.hpp"
#include "core/defaults/window.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/coordinate_transform.hpp"
#include "core/system/notifier.hpp"
@@ -22,7 +24,9 @@ namespace {
int width,
int height,
bool fullscreen,
int initial_vsync) -> bool {
int initial_vsync,
int render_width,
int render_height) -> bool {
// Título estático estilo CCAE. El FPS y el estado de VSync los muestra
// el DebugOverlay (toggle F11), no la barra de título.
const std::string TITLE = std::format("© 2026 {} — JailDesigner",
@@ -44,9 +48,13 @@ namespace {
}
// Inicializar el FrameRenderer (claim del window + pipeline de líneas).
// logical_*: espacio de coordenadas del juego (fijo 1280×720).
// render_*: resolución física del offscreen (configurable vía YAML).
if (!gpu_renderer.init(window,
static_cast<float>(Defaults::Game::WIDTH),
static_cast<float>(Defaults::Game::HEIGHT))) {
static_cast<float>(Defaults::Game::HEIGHT),
static_cast<float>(render_width),
static_cast<float>(render_height))) {
std::cerr << "Error inicialitzant GpuFrameRenderer\n";
SDL_DestroyWindow(window);
return false;
@@ -80,7 +88,30 @@ SDLManager::SDLManager(int width, int height, bool fullscreen, Config::EngineCon
calculateMaxWindowSize();
if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, is_fullscreen_, cfg_->rendering.vsync)) {
// Validar la resolució de render del config: si no és un preset 16:9
// conegut, fer fallback a 1280×720 i avisar. Això protegeix d'edicions
// manuals invàlides al YAML.
int effective_render_w = cfg_->rendering.render_width;
int effective_render_h = cfg_->rendering.render_height;
if (!Defaults::Rendering::isValidRenderResolution(effective_render_w, effective_render_h)) {
std::cerr << "Resolució de render invàlida (" << effective_render_w << "x"
<< effective_render_h << "), fent fallback a "
<< Defaults::Rendering::RENDER_WIDTH_DEFAULT << "x"
<< Defaults::Rendering::RENDER_HEIGHT_DEFAULT << '\n';
effective_render_w = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
effective_render_h = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
cfg_->rendering.render_width = effective_render_w;
cfg_->rendering.render_height = effective_render_h;
}
if (!initWindowAndGpu(&finestra_,
gpu_renderer_,
current_width_,
current_height_,
is_fullscreen_,
cfg_->rendering.vsync,
effective_render_w,
effective_render_h)) {
SDL_Quit();
return;
}
@@ -97,7 +128,9 @@ SDLManager::SDLManager(int width, int height, bool fullscreen, Config::EngineCon
std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_
<< " (logic: " << Defaults::Game::WIDTH << "x"
<< Defaults::Game::HEIGHT << ")";
<< Defaults::Game::HEIGHT
<< ", render: " << effective_render_w << "x" << effective_render_h
<< ")";
if (is_fullscreen_) {
std::cout << " [FULLSCREEN]";
}
+15 -23
View File
@@ -4,21 +4,13 @@
#include <string>
#include "core/defaults.hpp"
#include "core/types.hpp"
namespace System {
namespace {
// Posición y tamaño del overlay en coordenadas lógicas (1280×720).
constexpr float OVERLAY_X = 12.0F;
constexpr float OVERLAY_Y_FPS = 12.0F;
constexpr float OVERLAY_LINE_HEIGHT = 18.0F; // separación entre líneas (scale 0.4 → ~16 px alto)
constexpr float OVERLAY_SCALE = 0.4F;
constexpr float OVERLAY_SPACING = 2.0F;
constexpr float OVERLAY_BRIGHTNESS = 1.0F;
// Cadencia de actualización del valor de FPS mostrado.
constexpr float FPS_UPDATE_INTERVAL = 0.5F;
namespace Cfg = Defaults::Hud::DebugOverlay;
} // namespace
DebugOverlay::DebugOverlay(Rendering::Renderer* renderer,
@@ -30,7 +22,7 @@ namespace System {
fps_accumulator_ += delta_time;
fps_frame_count_++;
if (fps_accumulator_ >= FPS_UPDATE_INTERVAL) {
if (fps_accumulator_ >= Cfg::FPS_UPDATE_INTERVAL) {
fps_display_ = static_cast<int>(fps_frame_count_ / fps_accumulator_);
fps_frame_count_ = 0;
fps_accumulator_ = 0.0F;
@@ -47,20 +39,20 @@ namespace System {
const std::string AA_TEXT = std::string("AA: ") + (rendering_cfg_->antialias == 1 ? "ON" : "OFF");
text_.render(FPS_TEXT,
Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS},
OVERLAY_SCALE,
OVERLAY_SPACING,
OVERLAY_BRIGHTNESS);
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS},
Cfg::TEXT_SCALE,
Cfg::TEXT_SPACING,
Cfg::BRIGHTNESS);
text_.render(VSYNC_TEXT,
Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS + OVERLAY_LINE_HEIGHT},
OVERLAY_SCALE,
OVERLAY_SPACING,
OVERLAY_BRIGHTNESS);
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + Cfg::LINE_HEIGHT},
Cfg::TEXT_SCALE,
Cfg::TEXT_SPACING,
Cfg::BRIGHTNESS);
text_.render(AA_TEXT,
Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS + (2.0F * OVERLAY_LINE_HEIGHT)},
OVERLAY_SCALE,
OVERLAY_SPACING,
OVERLAY_BRIGHTNESS);
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + (2.0F * Cfg::LINE_HEIGHT)},
Cfg::TEXT_SCALE,
Cfg::TEXT_SPACING,
Cfg::BRIGHTNESS);
}
} // namespace System
+2 -1
View File
@@ -11,7 +11,8 @@
#include "core/audio/audio.hpp"
#include "core/audio/audio_adapter.hpp"
#include "core/defaults.hpp"
#include "core/defaults/audio.hpp"
#include "core/defaults/window.hpp"
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/sdl_manager.hpp"
+7 -7
View File
@@ -60,18 +60,18 @@ namespace GlobalEvents {
case SDL_SCANCODE_ESCAPE: {
// Doble pulsació per confirmar sortida: la primera ESC
// dispara un toast d'avís; mentre el toast està entrant
// o aguantant (isActiveWindow), la segona ESC tanca el
// joc. Si el toast ha començat a sortir o ja ha
// desaparegut, ESC torna a obrir la finestra de
// confirmació sense tancar.
// dispara un toast d'avís; només si aquest toast concret
// (isExitPromptActive) segueix visible, la segona ESC
// tanca el joc. Si la notificació activa és una altra
// (zoom, fullscreen, vsync...), ESC obre el prompt de
// sortida en lloc de tancar.
auto* notifier = System::Notifier::get();
if (notifier != nullptr && !notifier->isActiveWindow()) {
if (notifier != nullptr && !notifier->isExitPromptActive()) {
notifier->notifyExit("PREMEU ESC UN ALTRE COP PER EIXIR");
return true;
}
// Notifier inexistent (degradació elegant) o segona ESC
// dins la finestra activa: tanquem el joc.
// sobre el prompt de sortida: tanquem el joc.
context.setNextScene(SceneType::EXIT);
SceneManager::actual = SceneType::EXIT;
return true;
+34 -43
View File
@@ -2,24 +2,15 @@
#include "core/system/notifier.hpp"
#include "core/defaults.hpp"
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
#include "core/utils/easing.hpp"
namespace System {
namespace {
// Geometria del cuadre en coordenades lògiques (1280×720).
constexpr float CANVAS_WIDTH = 1280.0F;
constexpr float MARGIN_TOP = 40.0F;
constexpr float PADDING_H = 16.0F;
constexpr float PADDING_V = 10.0F;
constexpr float BORDER_THICKNESS = 2.0F;
constexpr float TEXT_SCALE = 0.55F;
constexpr float TEXT_SPACING = 2.0F;
constexpr float BORDER_BRIGHTNESS = 1.0F;
// Cinemàtica del slide.
constexpr float SLIDE_DURATION_S = 0.30F;
// Alias d'àmbit local per accedir compactament als defaults del notifier.
namespace Cfg = Defaults::Notifier;
// Conversió color SDL → float [0,1].
constexpr auto toUnit(Uint8 v) -> float {
@@ -47,14 +38,6 @@ namespace System {
.b = toUnit(c.b) * DARKEN,
.a = BG_ALPHA};
}
// Presets per als atajos semàntics.
constexpr SDL_Color COLOR_INFO{.r = 80, .g = 230, .b = 255, .a = 255};
constexpr SDL_Color COLOR_WARN{.r = 255, .g = 180, .b = 40, .a = 255};
constexpr SDL_Color COLOR_EXIT{.r = 255, .g = 80, .b = 80, .a = 255};
constexpr float DURATION_INFO = 2.0F;
constexpr float DURATION_WARN = 3.0F;
constexpr float DURATION_EXIT = 3.0F;
} // namespace
std::unique_ptr<Notifier> Notifier::instance;
@@ -77,16 +60,17 @@ namespace System {
current_text_ = text;
current_color_ = text_color;
hold_remaining_s_ = duration_s;
current_is_exit_ = false;
const float TEXT_W = Graphics::VectorText::getTextWidth(text, TEXT_SCALE, TEXT_SPACING);
const float TEXT_H = Graphics::VectorText::getTextHeight(TEXT_SCALE);
const float TEXT_W = Graphics::VectorText::getTextWidth(text, Cfg::TEXT_SCALE, Cfg::TEXT_SPACING);
const float TEXT_H = Graphics::VectorText::getTextHeight(Cfg::TEXT_SCALE);
box_w_ = TEXT_W + (PADDING_H * 2.0F);
box_h_ = TEXT_H + (PADDING_V * 2.0F);
text_x_ = (CANVAS_WIDTH - TEXT_W) * 0.5F;
box_w_ = TEXT_W + (Cfg::PADDING_H * 2.0F);
box_h_ = TEXT_H + (Cfg::PADDING_V * 2.0F);
text_x_ = (Cfg::CANVAS_WIDTH - TEXT_W) * 0.5F;
y_on_ = MARGIN_TOP;
y_off_ = -(box_h_ + BORDER_THICKNESS);
y_on_ = Cfg::MARGIN_TOP;
y_off_ = -(box_h_ + Cfg::BORDER_THICKNESS);
// Si ja es veu, reseteja el slide-in des de la posició actual perquè
// la transició sembli continua. Si està amagat, arrenc des de fora.
@@ -95,23 +79,26 @@ namespace System {
}
status_ = Status::ENTERING;
slide_elapsed_s_ = 0.0F;
text_scale_ = TEXT_SCALE;
text_scale_ = Cfg::TEXT_SCALE;
}
void Notifier::notifyInfo(const std::string& text) { notify(text, COLOR_INFO, DURATION_INFO); }
void Notifier::notifyWarn(const std::string& text) { notify(text, COLOR_WARN, DURATION_WARN); }
void Notifier::notifyExit(const std::string& text) { notify(text, COLOR_EXIT, DURATION_EXIT); }
void Notifier::notifyInfo(const std::string& text) { notify(text, Cfg::COLOR_INFO, Cfg::DURATION_INFO); }
void Notifier::notifyWarn(const std::string& text) { notify(text, Cfg::COLOR_WARN, Cfg::DURATION_WARN); }
void Notifier::notifyExit(const std::string& text) {
notify(text, Cfg::COLOR_EXIT, Cfg::DURATION_EXIT);
current_is_exit_ = true; // notify() ho ha posat a false; restaurem.
}
void Notifier::update(float delta_time) {
switch (status_) {
case Status::ENTERING: {
slide_elapsed_s_ += delta_time;
if (slide_elapsed_s_ >= SLIDE_DURATION_S) {
if (slide_elapsed_s_ >= Cfg::SLIDE_DURATION_S) {
y_current_ = y_on_;
status_ = Status::HOLDING;
slide_elapsed_s_ = 0.0F;
} else {
const float T = slide_elapsed_s_ / SLIDE_DURATION_S;
const float T = slide_elapsed_s_ / Cfg::SLIDE_DURATION_S;
const float K = Utils::Easing::outCubic(T);
y_current_ = y_off_ + ((y_on_ - y_off_) * K);
}
@@ -127,11 +114,11 @@ namespace System {
}
case Status::EXITING: {
slide_elapsed_s_ += delta_time;
if (slide_elapsed_s_ >= SLIDE_DURATION_S) {
if (slide_elapsed_s_ >= Cfg::SLIDE_DURATION_S) {
y_current_ = y_off_;
status_ = Status::HIDDEN;
} else {
const float T = slide_elapsed_s_ / SLIDE_DURATION_S;
const float T = slide_elapsed_s_ / Cfg::SLIDE_DURATION_S;
const float K = Utils::Easing::inCubic(T);
y_current_ = y_on_ + ((y_off_ - y_on_) * K);
}
@@ -148,7 +135,7 @@ namespace System {
return;
}
const float BOX_X = (CANVAS_WIDTH - box_w_) * 0.5F;
const float BOX_X = (Cfg::CANVAS_WIDTH - box_w_) * 0.5F;
const float BOX_Y = y_current_;
const UnitRGBA TC = textColorFloat(current_color_);
const UnitRGBA BG = bgColorFloat(current_color_);
@@ -163,19 +150,19 @@ namespace System {
const float Y1 = BOX_Y;
const float X2 = BOX_X + box_w_;
const float Y2 = BOX_Y + box_h_;
gpu->pushLine(X1, Y1, X2, Y1, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // top
gpu->pushLine(X1, Y2, X2, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // bottom
gpu->pushLine(X1, Y1, X1, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // left
gpu->pushLine(X2, Y1, X2, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // right
gpu->pushLine(X1, Y1, X2, Y1, Cfg::BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // top
gpu->pushLine(X1, Y2, X2, Y2, Cfg::BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // bottom
gpu->pushLine(X1, Y1, X1, Y2, Cfg::BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // left
gpu->pushLine(X2, Y1, X2, Y2, Cfg::BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // right
// 3. Text centrat dins la caixa, amb color explícit (l'alpha != 0
// li diu al renderShape que no agafe l'oscil·lador global de color).
const float TEXT_Y = BOX_Y + PADDING_V;
const float TEXT_Y = BOX_Y + Cfg::PADDING_V;
text_.render(current_text_,
Vec2{.x = text_x_, .y = TEXT_Y},
text_scale_,
TEXT_SPACING,
BORDER_BRIGHTNESS,
Cfg::TEXT_SPACING,
Cfg::BORDER_BRIGHTNESS,
current_color_);
}
@@ -183,4 +170,8 @@ namespace System {
return status_ == Status::ENTERING || status_ == Status::HOLDING;
}
auto Notifier::isExitPromptActive() const -> bool {
return isActiveWindow() && current_is_exit_;
}
} // namespace System
+8 -2
View File
@@ -47,10 +47,15 @@ namespace System {
void draw() const;
// Activa mentre el toast està entrant o aguantant. Quan està sortint
// o ja amagat, retorna false. Útil per a la lògica de doble-pulsació
// d'ESC: la segona pulsació només confirma sortida si encara aguanta.
// o ja amagat, retorna false.
[[nodiscard]] auto isActiveWindow() const -> bool;
// Cert només si el toast actiu va ser disparat per notifyExit().
// Per a la doble-pulsació d'ESC: la segona ESC confirma sortida
// únicament si la notificació visible és la de confirmació; si era
// de F1/F2/etc., ESC torna a obrir el prompt sense tancar.
[[nodiscard]] auto isExitPromptActive() const -> bool;
private:
explicit Notifier(Rendering::Renderer* renderer);
@@ -74,6 +79,7 @@ namespace System {
float box_h_{0.0F};
float text_x_{0.0F}; // X esquerre del text dins la caixa
float text_scale_{0.4F};
bool current_is_exit_{false}; // true només si l'actiu ve de notifyExit()
static std::unique_ptr<Notifier> instance;
};
+31 -2
View File
@@ -5,7 +5,8 @@
#include <string>
#include <unordered_map>
#include "core/defaults.hpp"
#include "core/defaults/rendering.hpp"
#include "core/defaults/window.hpp"
#include "external/fkyaml_node.hpp"
#include "project.h"
@@ -204,6 +205,8 @@ namespace ConfigYaml {
// Rendering
rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT;
rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
// Version
version = std::string(Project::VERSION);
@@ -275,6 +278,28 @@ namespace ConfigYaml {
rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT;
}
}
// Resolució de render: validem el parell (w, h) contra la llista
// tancada de presets 16:9. Si falla l'una o l'altra, fem fallback
// dels dos camps al default per mantenir un parell vàlid.
int candidate_w = rendering.render_width;
int candidate_h = rendering.render_height;
readField(rend, "render_width", candidate_w, Defaults::Rendering::RENDER_WIDTH_DEFAULT);
readField(rend, "render_height", candidate_h, Defaults::Rendering::RENDER_HEIGHT_DEFAULT);
if (Defaults::Rendering::isValidRenderResolution(candidate_w, candidate_h)) {
rendering.render_width = candidate_w;
rendering.render_height = candidate_h;
} else {
if (console) {
std::cerr << "Resolució de render invàlida al YAML ("
<< candidate_w << "x" << candidate_h
<< "), fallback a "
<< Defaults::Rendering::RENDER_WIDTH_DEFAULT << "x"
<< Defaults::Rendering::RENDER_HEIGHT_DEFAULT << '\n';
}
rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
}
}
}
@@ -501,7 +526,11 @@ namespace ConfigYaml {
file << "# RENDERITZACIÓ\n";
file << "rendering:\n";
file << " vsync: " << rendering.vsync << " # 0=disabled, 1=enabled\n\n";
file << " vsync: " << rendering.vsync << " # 0=disabled, 1=enabled\n";
file << " render_width: " << rendering.render_width
<< " # Presets 16:9: 1280, 1600, 1920, 2560, 3840 (fallback 1280)\n";
file << " render_height: " << rendering.render_height
<< " # Parell amb render_width (720, 900, 1080, 1440, 2160)\n\n";
// Guardar controls de jugadors
savePlayer1ControlsToYaml(file);
+26 -12
View File
@@ -8,26 +8,40 @@
namespace Effects {
// Debris: un segment de línia que vola perpendicular a sí mismo
// Representa un fragment de una shape destruïda (ship, enemy, bullet)
struct Debris {
// Geometria del segment (2 points en coordenades mundials)
Vec2 p1; // Vec2 inicial del segment
Vec2 p2; // Vec2 final del segment
// Debris: un segment de línia que vola perpendicular a sí mismo
// Representa un fragment de una shape destruïda (ship, enemy, bullet).
//
// Representació autoritaritzada (font de veritat per a la geometria):
// centro + original_angle (orientació spawn) + angle_rotacio (acumulat)
// + original_half_length × shrink_factor (mida actual)
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades, mai
// de forma iterativa — evita bugs de rotació quadràtica i shrink exponencial.
struct Debris {
// Geometria del segment (2 points en coordenades mundials, derivats)
Vec2 p1;
Vec2 p2;
// Geometria original (font de veritat, no canvia després del spawn)
Vec2 centro; // Centre actual del segment (es mou amb velocity)
float original_angle; // Orientació del segment al spawn (rad)
float original_half_length; // Mitja-longitud al spawn (px)
// Física
Vec2 velocity; // Velocidad en px/s (components x, y)
Vec2 velocity; // Velocidad en px/s (components x, y)
float acceleration; // Aceleración negativa (fricció) en px/s²
// Rotación
float angle_rotacio; // Angle de rotación acumulat (radians)
float angle_rotacio; // Acumulat de rotació visual des del spawn (radians)
float velocitat_rot; // Velocidad de rotación de TRAYECTORIA (rad/s)
float velocitat_rot_visual; // Velocidad de rotación VISUAL del segment (rad/s)
// Estat de vida
float temps_vida; // Temps transcorregut (segons)
float temps_max; // Temps de vida màxim (segons)
bool active; // Está active?
// Política: viu sempre durant min_lifetime, després mor quan
// |velocity| < MIN_SPEED_TO_DIE (definit en Defaults). Així els
// fragments ràpids no "popen" en moviment.
float temps_vida; // Temps transcorregut (segons)
float min_lifetime; // Temps mínim garantit (segons)
bool active; // Està actiu?
// Shrinking (reducció de distancia entre points)
float factor_shrink; // Factor de reducció per segon (0.0-1.0)
@@ -35,6 +49,6 @@ struct Debris {
// Rendering
float brightness; // Factor de brightness (0.0-1.0, heretat de l'objecte original)
SDL_Color color{}; // Color heredado del padre. alpha==0 → usa global oscilador
};
};
} // namespace Effects
+371 -320
View File
@@ -14,375 +14,426 @@
namespace Effects {
// Helper: transformar point con rotación, scale i traslación
// (Copiat de shape_renderer.cpp:12-34)
static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 {
// 1. Centrar el point respecte al centro de la shape
float centered_x = point.x - shape_centre.x;
float centered_y = point.y - shape_centre.y;
// Helper: transformar point con rotación, scale i traslación
// (Copiat de shape_renderer.cpp:12-34)
static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 {
// 1. Centrar el point respecte al centro de la shape
float centered_x = point.x - shape_centre.x;
float centered_y = point.y - shape_centre.y;
// 2. Aplicar scale al point centrat
float scaled_x = centered_x * scale;
float scaled_y = centered_y * scale;
// 2. Aplicar scale al point centrat
float scaled_x = centered_x * scale;
float scaled_y = centered_y * scale;
// 3. Aplicar rotación
float cos_a = std::cos(angle);
float sin_a = std::sin(angle);
// 3. Aplicar rotación
float cos_a = std::cos(angle);
float sin_a = std::sin(angle);
float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a);
float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a);
float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a);
float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a);
// 4. Aplicar traslación a posición mundial
return {.x = rotated_x + position.x, .y = rotated_y + position.y};
}
DebrisManager::DebrisManager(Rendering::Renderer* renderer)
: renderer_(renderer) {
// Inicialitzar todos los debris como inactius
for (auto& debris : debris_pool_) {
debris.active = false;
}
}
void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& centro,
float angle,
float scale,
float velocitat_base,
float brightness,
const Vec2& velocitat_objecte,
float velocitat_angular,
float factor_herencia_visual,
const std::string& sound,
SDL_Color color) {
if (!shape || !shape->isValid()) {
return;
// 4. Aplicar traslación a posición mundial
return {.x = rotated_x + position.x, .y = rotated_y + position.y};
}
// Reproducir sonido de explosión
Audio::get()->playSound(sound, Audio::Group::GAME);
DebrisManager::DebrisManager(Rendering::Renderer* renderer)
: renderer_(renderer) {
// Inicialitzar todos los debris como inactius
for (auto& debris : debris_pool_) {
debris.active = false;
}
}
const Vec2& shape_centre = shape->getCenter();
void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& centro,
float angle,
float scale,
float velocitat_base,
float brightness,
const Vec2& velocitat_objecte,
float velocitat_angular,
float factor_herencia_visual,
const std::string& sound,
SDL_Color color,
float lifetime,
float friction,
int segment_multiplier) {
if (!shape || !shape->isValid()) {
return;
}
for (const auto& primitive : shape->getPrimitives()) {
for (const auto& [local_p1, local_p2] : extractSegments(primitive)) {
// Transformar points locals → coordenades mundials
Vec2 world_p1 = transformPoint(local_p1, shape_centre, centro, angle, scale);
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
// Reproducir sonido de explosión
Audio::get()->playSound(sound, Audio::Group::GAME);
// Si el pool es ple, no té sentit continuar amb la resta de segments
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness,
velocitat_objecte, velocitat_angular,
factor_herencia_visual, color)) {
return;
const Vec2& shape_centre = shape->getCenter();
// Multiplier: cada segment s'emet N vegades amb direccions aleatòries
// distintes (la variació ±15° de computeExplosionDirection ho garanteix).
const int COPIES = std::max(1, segment_multiplier);
for (int copy = 0; copy < COPIES; copy++) {
for (const auto& primitive : shape->getPrimitives()) {
for (const auto& [local_p1, local_p2] : extractSegments(primitive)) {
// Transformar points locals → coordenades mundials
Vec2 world_p1 = transformPoint(local_p1, shape_centre, centro, angle, scale);
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
// Si el pool es ple, no té sentit continuar amb la resta de segments
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction)) {
return;
}
}
}
}
}
}
auto DebrisManager::extractSegments(const Graphics::ShapePrimitive& primitive)
-> std::vector<std::pair<Vec2, Vec2>> {
std::vector<std::pair<Vec2, Vec2>> segments;
auto DebrisManager::extractSegments(const Graphics::ShapePrimitive& primitive)
-> std::vector<std::pair<Vec2, Vec2>> {
std::vector<std::pair<Vec2, Vec2>> segments;
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
// Polyline: extreure segments consecutius
for (size_t i = 0; i + 1 < primitive.points.size(); i++) {
segments.emplace_back(primitive.points[i], primitive.points[i + 1]);
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
// Polyline: extreure segments consecutius
for (size_t i = 0; i + 1 < primitive.points.size(); i++) {
segments.emplace_back(primitive.points[i], primitive.points[i + 1]);
}
return segments;
}
// PrimitiveType::LINE: un únic segment (si té els 2 punts)
if (primitive.points.size() >= 2) {
segments.emplace_back(primitive.points[0], primitive.points[1]);
}
return segments;
}
// PrimitiveType::LINE: un únic segment (si té els 2 punts)
if (primitive.points.size() >= 2) {
segments.emplace_back(primitive.points[0], primitive.points[1]);
}
return segments;
}
auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2,
const Vec2& centro, float velocitat_base, float brightness,
const Vec2& velocitat_objecte, float velocitat_angular,
float factor_herencia_visual, SDL_Color color) -> bool {
Debris* debris = findFreeSlot();
if (debris == nullptr) {
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
return false;
}
// Geometria
debris->p1 = world_p1;
debris->p2 = world_p2;
// Direcció radial (desde el centro hacia el segment)
Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro);
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte)
float speed =
velocitat_base +
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
Defaults::Physics::Debris::VARIACIO_VELOCITAT);
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
debris->acceleration = Defaults::Physics::Debris::ACCELERACIO;
// Rotación de trayectoria (con conversió a tangencial si excedeix cap)
applyAngularVelocity(*debris, direccio, velocitat_angular);
// Rotación visual (proporcional o aleatòria)
applyVisualRotation(*debris, velocitat_angular, factor_herencia_visual);
debris->angle_rotacio = 0.0F;
// Vida i shrinking
debris->temps_vida = 0.0F;
debris->temps_max = Defaults::Physics::Debris::TEMPS_VIDA;
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
// Visuals heretades
debris->brightness = brightness;
debris->color = color;
debris->active = true;
return true;
}
void DebrisManager::applyAngularVelocity(Debris& debris, const Vec2& direccio,
float velocitat_angular) {
if (std::abs(velocitat_angular) <= 0.01F) {
debris.velocitat_rot = 0.0F; // Nave: sin curvas
return;
}
// FASE 1: Aplicar herència i variació
float factor_herencia =
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::FACTOR_HERENCIA_MAX -
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN));
float velocitat_ang_heretada = velocitat_angular * factor_herencia;
float variacio = ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F;
velocitat_ang_heretada *= (1.0F + variacio);
// FASE 2: Cap a la velocity màxima; l'excés es converteix en tangencial
constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX;
float abs_ang = std::abs(velocitat_ang_heretada);
float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F;
if (abs_ang <= CAP) {
debris.velocitat_rot = velocitat_ang_heretada;
return;
}
// Excés: converteix l'excés de velocitat angular en velocitat tangencial lineal
float excess = abs_ang - CAP;
constexpr float RADIUS = 20.0F; // Radi típic de la shape (enemigos = 20 px)
float v_tangential = excess * RADIUS;
// Direcció tangencial: perpendicular a la radial (90° CCW): tangent = (-dy, dx)
debris.velocity.x += -direccio.y * v_tangential;
debris.velocity.y += direccio.x * v_tangential;
// Velocitat angular limitada al cap (preservant el signe)
debris.velocitat_rot = sign_ang * CAP;
}
void DebrisManager::applyVisualRotation(Debris& debris, float velocitat_angular,
float factor_herencia_visual) {
if (factor_herencia_visual > 0.01F && std::abs(velocitat_angular) > 0.01F) {
// Heredar rotación visual con factor proporcional + ±5% de variació
debris.velocitat_rot_visual = debris.velocitat_rot * factor_herencia_visual;
float variacio_visual =
((std::rand() / static_cast<float>(RAND_MAX)) * 0.1F) - 0.05F;
debris.velocitat_rot_visual *= (1.0F + variacio_visual);
return;
}
// Rotación visual aleatòria (factor = 0.0 o sin velocidad angular)
debris.velocitat_rot_visual =
Defaults::Physics::Debris::ROTACIO_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::ROTACIO_MAX -
Defaults::Physics::Debris::ROTACIO_MIN));
// 50% probabilitat de rotación en sentit contrari
if (std::rand() % 2 == 0) {
debris.velocitat_rot_visual = -debris.velocitat_rot_visual;
}
}
void DebrisManager::update(float delta_time) {
for (auto& debris : debris_pool_) {
if (!debris.active) {
continue;
auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool {
Debris* debris = findFreeSlot();
if (debris == nullptr) {
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
return false;
}
// 1. Actualitzar time de vida
debris.temps_vida += delta_time;
// Geometria autoritaritzada: centro + original_angle + original_half_length.
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades.
const float DX = world_p2.x - world_p1.x;
const float DY = world_p2.y - world_p1.y;
debris->centro = {.x = (world_p1.x + world_p2.x) / 2.0F,
.y = (world_p1.y + world_p2.y) / 2.0F};
debris->original_angle = std::atan2(DY, DX);
debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F;
debris->p1 = world_p1;
debris->p2 = world_p2;
// Desactivar si ha superat time màxim
if (debris.temps_vida >= debris.temps_max) {
debris.active = false;
continue;
// Direcció radial (desde el centro hacia el segment)
Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro);
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte)
float speed =
velocitat_base +
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
Defaults::Physics::Debris::VARIACIO_VELOCITAT);
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
debris->acceleration = friction;
// Rotación de trayectoria (con conversió a tangencial si excedeix cap)
applyAngularVelocity(*debris, direccio, velocitat_angular);
// Rotación visual (proporcional o aleatòria)
applyVisualRotation(*debris, velocitat_angular, factor_herencia_visual);
debris->angle_rotacio = 0.0F;
// Vida i shrinking — min_lifetime és el temps mínim garantit; després
// el fragment mor quan |velocity| < MIN_SPEED_TO_DIE.
debris->temps_vida = 0.0F;
debris->min_lifetime = lifetime;
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
// Visuals heretades
debris->brightness = brightness;
debris->color = color;
debris->active = true;
return true;
}
void DebrisManager::applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular) {
if (std::abs(velocitat_angular) <= 0.01F) {
debris.velocitat_rot = 0.0F; // Nave: sin curvas
return;
}
// 2. Actualitzar velocity (desacceleració)
// Aplicar fricció en la direcció del movement
float speed = std::sqrt((debris.velocity.x * debris.velocity.x) +
(debris.velocity.y * debris.velocity.y));
// FASE 1: Aplicar herència i variació
float factor_herencia =
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::FACTOR_HERENCIA_MAX -
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN));
float velocitat_ang_heretada = velocitat_angular * factor_herencia;
float variacio = ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F;
velocitat_ang_heretada *= (1.0F + variacio);
if (speed > 1.0F) {
// Calcular direcció normalitzada
float dir_x = debris.velocity.x / speed;
float dir_y = debris.velocity.y / speed;
// FASE 2: Cap a la velocity màxima; l'excés es converteix en tangencial
constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX;
float abs_ang = std::abs(velocitat_ang_heretada);
float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F;
// Aplicar aceleración negativa (fricció)
float nova_speed = speed + (debris.acceleration * delta_time);
nova_speed = std::max(nova_speed, 0.0F);
debris.velocity.x = dir_x * nova_speed;
debris.velocity.y = dir_y * nova_speed;
} else {
// Velocidad mucho baixa, aturar
debris.velocity.x = 0.0F;
debris.velocity.y = 0.0F;
if (abs_ang <= CAP) {
debris.velocitat_rot = velocitat_ang_heretada;
return;
}
// 2b. Rotar vector de velocity (trayectoria curva)
if (std::abs(debris.velocitat_rot) > 0.01F) {
// Calcular angle de rotación este frame
float dangle = debris.velocitat_rot * delta_time;
// Excés: converteix l'excés de velocitat angular en velocitat tangencial lineal
float excess = abs_ang - CAP;
constexpr float RADIUS = 20.0F; // Radi típic de la shape (enemigos = 20 px)
float v_tangential = excess * RADIUS;
// Rotar vector de velocity usant matriu de rotación 2D
float vel_x_old = debris.velocity.x;
float vel_y_old = debris.velocity.y;
// Direcció tangencial: perpendicular a la radial (90° CCW): tangent = (-dy, dx)
debris.velocity.x += -direccio.y * v_tangential;
debris.velocity.y += direccio.x * v_tangential;
float cos_a = std::cos(dangle);
float sin_a = std::sin(dangle);
// Velocitat angular limitada al cap (preservant el signe)
debris.velocitat_rot = sign_ang * CAP;
}
debris.velocity.x = (vel_x_old * cos_a) - (vel_y_old * sin_a);
debris.velocity.y = (vel_x_old * sin_a) + (vel_y_old * cos_a);
void DebrisManager::applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual) {
if (factor_herencia_visual > 0.01F && std::abs(velocitat_angular) > 0.01F) {
// Heredar rotación visual con factor proporcional + ±5% de variació
debris.velocitat_rot_visual = debris.velocitat_rot * factor_herencia_visual;
float variacio_visual =
((std::rand() / static_cast<float>(RAND_MAX)) * 0.1F) - 0.05F;
debris.velocitat_rot_visual *= (1.0F + variacio_visual);
return;
}
// 2c. Aplicar fricció angular (desacceleració gradual)
if (std::abs(debris.velocitat_rot) > 0.01F) {
float sign = (debris.velocitat_rot > 0) ? 1.0F : -1.0F;
float reduccion =
Defaults::Physics::Debris::FRICCIO_ANGULAR * delta_time;
debris.velocitat_rot -= sign * reduccion;
// Rotación visual aleatòria (factor = 0.0 o sin velocidad angular)
debris.velocitat_rot_visual =
Defaults::Physics::Debris::ROTACIO_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::ROTACIO_MAX -
Defaults::Physics::Debris::ROTACIO_MIN));
// Evitar canvi de signe (no pot passar de CW a CCW)
if ((debris.velocitat_rot > 0) != (sign > 0)) {
debris.velocitat_rot = 0.0F;
// 50% probabilitat de rotación en sentit contrari
if (std::rand() % 2 == 0) {
debris.velocitat_rot_visual = -debris.velocitat_rot_visual;
}
}
// Rebot del fragment contra els límits del PLAYAREA (mateix patró que
// PhysicsWorld::resolveBoundsCollisions per a enemics i ships).
static void bounceOffPlayArea(Vec2& centro, Vec2& velocity) {
const auto& bounds = Defaults::Zones::PLAYAREA;
const float MIN_X = bounds.x;
const float MAX_X = bounds.x + bounds.w;
const float MIN_Y = bounds.y;
const float MAX_Y = bounds.y + bounds.h;
constexpr float REST = Defaults::Physics::Debris::RESTITUTION_BOUNDS;
if (centro.x < MIN_X) {
centro.x = MIN_X;
if (velocity.x < 0.0F) {
velocity.x = -velocity.x * REST;
}
}
// 3. Calcular centro del segment
Vec2 centro = {.x = (debris.p1.x + debris.p2.x) / 2.0F,
.y = (debris.p1.y + debris.p2.y) / 2.0F};
// 4. Actualitzar posición del centro
centro.x += debris.velocity.x * delta_time;
centro.y += debris.velocity.y * delta_time;
// 5. Actualitzar rotación VISUAL
debris.angle_rotacio += debris.velocitat_rot_visual * delta_time;
// 6. Aplicar shrinking (reducció de distancia entre points)
float shrink_factor =
1.0F - (debris.factor_shrink * debris.temps_vida / debris.temps_max);
shrink_factor = std::max(0.0F, shrink_factor); // No negatiu
// Calcular distancia original entre points
float dx = debris.p2.x - debris.p1.x;
float dy = debris.p2.y - debris.p1.y;
// 7. Reconstruir segment con nueva mida i rotación
float half_length = std::sqrt((dx * dx) + (dy * dy)) * shrink_factor / 2.0F;
float original_angle = std::atan2(dy, dx);
float new_angle = original_angle + debris.angle_rotacio;
debris.p1.x = centro.x - (half_length * std::cos(new_angle));
debris.p1.y = centro.y - (half_length * std::sin(new_angle));
debris.p2.x = centro.x + (half_length * std::cos(new_angle));
debris.p2.y = centro.y + (half_length * std::sin(new_angle));
}
}
void DebrisManager::draw() const {
for (const auto& debris : debris_pool_) {
if (!debris.active) {
continue;
if (centro.x > MAX_X) {
centro.x = MAX_X;
if (velocity.x > 0.0F) {
velocity.x = -velocity.x * REST;
}
}
// Dibujar segmento con brightness y color heredados del padre.
Rendering::linea(renderer_,
static_cast<int>(debris.p1.x),
static_cast<int>(debris.p1.y),
static_cast<int>(debris.p2.x),
static_cast<int>(debris.p2.y),
debris.brightness, 0.0F, debris.color);
}
}
auto DebrisManager::findFreeSlot() -> Debris* {
for (auto& debris : debris_pool_) {
if (!debris.active) {
return &debris;
if (centro.y < MIN_Y) {
centro.y = MIN_Y;
if (velocity.y < 0.0F) {
velocity.y = -velocity.y * REST;
}
}
if (centro.y > MAX_Y) {
centro.y = MAX_Y;
if (velocity.y > 0.0F) {
velocity.y = -velocity.y * REST;
}
}
}
return nullptr; // Pool ple
}
auto DebrisManager::computeExplosionDirection(const Vec2& p1,
const Vec2& p2,
const Vec2& centre_objecte) -> Vec2 {
// 1. Calcular centro del segment
float centro_seg_x = (p1.x + p2.x) / 2.0F;
float centro_seg_y = (p1.y + p2.y) / 2.0F;
void DebrisManager::update(float delta_time) {
for (auto& debris : debris_pool_) {
if (!debris.active) {
continue;
}
// 2. Calcular vector des del centro de l'objecte hacia el centro del segment
// Això garanteix que la direcció siempre apunte hacia fuera (direcció radial)
float dx = centro_seg_x - centre_objecte.x;
float dy = centro_seg_y - centre_objecte.y;
// 1. Actualitzar time de vida
debris.temps_vida += delta_time;
// 3. Normalitzar (obtenir vector unitari)
float length = std::sqrt((dx * dx) + (dy * dy));
if (length < 0.001F) {
// Segment al centro (cas extrem mucho improbable), retornar direcció aleatòria
float angle_rand =
(std::rand() / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
return {.x = std::cos(angle_rand), .y = std::sin(angle_rand)};
}
// Política de mort: viu sí o sí durant min_lifetime; després mor
// quan la velocity cau per sota d'un llindar. Així els fragments
// ràpids no desapareixen en moviment.
if (debris.temps_vida >= debris.min_lifetime) {
const float SPEED_SQ = (debris.velocity.x * debris.velocity.x) +
(debris.velocity.y * debris.velocity.y);
if (SPEED_SQ < Defaults::Physics::Debris::MIN_SPEED_TO_DIE_SQ) {
debris.active = false;
continue;
}
}
dx /= length;
dy /= length;
// 2. Actualitzar velocity (desacceleració)
// Aplicar fricció en la direcció del movement
float speed = std::sqrt((debris.velocity.x * debris.velocity.x) +
(debris.velocity.y * debris.velocity.y));
// 4. Añadir variació aleatòria pequeña (±15°) per varietat visual
float angle_variacio =
((std::rand() % 30) - 15) * Defaults::Math::PI / 180.0F;
if (speed > 1.0F) {
// Calcular direcció normalitzada
float dir_x = debris.velocity.x / speed;
float dir_y = debris.velocity.y / speed;
float cos_v = std::cos(angle_variacio);
float sin_v = std::sin(angle_variacio);
// Aplicar aceleración negativa (fricció)
float nova_speed = speed + (debris.acceleration * delta_time);
nova_speed = std::max(nova_speed, 0.0F);
float final_x = (dx * cos_v) - (dy * sin_v);
float final_y = (dx * sin_v) + (dy * cos_v);
debris.velocity.x = dir_x * nova_speed;
debris.velocity.y = dir_y * nova_speed;
} else {
// Velocidad mucho baixa, aturar
debris.velocity.x = 0.0F;
debris.velocity.y = 0.0F;
}
return {.x = final_x, .y = final_y};
}
// 2b. Rotar vector de velocity (trayectoria curva)
if (std::abs(debris.velocitat_rot) > 0.01F) {
// Calcular angle de rotación este frame
float dangle = debris.velocitat_rot * delta_time;
void DebrisManager::reset() {
for (auto& debris : debris_pool_) {
debris.active = false;
}
}
// Rotar vector de velocity usant matriu de rotación 2D
float vel_x_old = debris.velocity.x;
float vel_y_old = debris.velocity.y;
auto DebrisManager::getActiveCount() const -> int {
int count = 0;
for (const auto& debris : debris_pool_) {
if (debris.active) {
count++;
float cos_a = std::cos(dangle);
float sin_a = std::sin(dangle);
debris.velocity.x = (vel_x_old * cos_a) - (vel_y_old * sin_a);
debris.velocity.y = (vel_x_old * sin_a) + (vel_y_old * cos_a);
}
// 2c. Aplicar fricció angular (desacceleració gradual)
if (std::abs(debris.velocitat_rot) > 0.01F) {
float sign = (debris.velocitat_rot > 0) ? 1.0F : -1.0F;
float reduccion =
Defaults::Physics::Debris::FRICCIO_ANGULAR * delta_time;
debris.velocitat_rot -= sign * reduccion;
// Evitar canvi de signe (no pot passar de CW a CCW)
if ((debris.velocitat_rot > 0) != (sign > 0)) {
debris.velocitat_rot = 0.0F;
}
}
// 3. Actualitzar posició del centre (integra velocity).
debris.centro.x += debris.velocity.x * delta_time;
debris.centro.y += debris.velocity.y * delta_time;
// 4. Rebot contra els límits del PLAYAREA.
bounceOffPlayArea(debris.centro, debris.velocity);
// 5. Actualitzar rotació visual acumulada.
debris.angle_rotacio += debris.velocitat_rot_visual * delta_time;
// 6. Shrink lineal sobre la longitud ORIGINAL (no iteratiu).
// SHRINK_T va de 0 a 1 al llarg de min_lifetime; després queda
// a 1 i el shrink_factor manté el valor mínim (1 - factor_shrink).
const float SHRINK_T = std::min(debris.temps_vida / debris.min_lifetime, 1.0F);
const float SHRINK_FACTOR = std::max(0.0F, 1.0F - (debris.factor_shrink * SHRINK_T));
// 7. Reconstruir p1/p2 des de la geometria autoritaritzada:
// centro + (cos/sin(original_angle + angle_rotacio)) × original_half_length × shrink_factor
// No iteratiu — evita la rotació quadràtica i el shrink exponencial.
const float CURRENT_ANGLE = debris.original_angle + debris.angle_rotacio;
const float HALF_LEN = debris.original_half_length * SHRINK_FACTOR;
const float COS_A = std::cos(CURRENT_ANGLE);
const float SIN_A = std::sin(CURRENT_ANGLE);
debris.p1.x = debris.centro.x - (HALF_LEN * COS_A);
debris.p1.y = debris.centro.y - (HALF_LEN * SIN_A);
debris.p2.x = debris.centro.x + (HALF_LEN * COS_A);
debris.p2.y = debris.centro.y + (HALF_LEN * SIN_A);
}
}
return count;
}
void DebrisManager::draw() const {
for (const auto& debris : debris_pool_) {
if (!debris.active) {
continue;
}
// Dibujar segmento con brightness y color heredados del padre.
Rendering::linea(renderer_,
static_cast<int>(debris.p1.x),
static_cast<int>(debris.p1.y),
static_cast<int>(debris.p2.x),
static_cast<int>(debris.p2.y),
debris.brightness,
0.0F,
debris.color);
}
}
auto DebrisManager::findFreeSlot() -> Debris* {
for (auto& debris : debris_pool_) {
if (!debris.active) {
return &debris;
}
}
return nullptr; // Pool ple
}
auto DebrisManager::computeExplosionDirection(const Vec2& p1,
const Vec2& p2,
const Vec2& centre_objecte) -> Vec2 {
// 1. Calcular centro del segment
float centro_seg_x = (p1.x + p2.x) / 2.0F;
float centro_seg_y = (p1.y + p2.y) / 2.0F;
// 2. Calcular vector des del centro de l'objecte hacia el centro del segment
// Això garanteix que la direcció siempre apunte hacia fuera (direcció radial)
float dx = centro_seg_x - centre_objecte.x;
float dy = centro_seg_y - centre_objecte.y;
// 3. Normalitzar (obtenir vector unitari)
float length = std::sqrt((dx * dx) + (dy * dy));
if (length < 0.001F) {
// Segment al centro (cas extrem mucho improbable), retornar direcció aleatòria
float angle_rand =
(std::rand() / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
return {.x = std::cos(angle_rand), .y = std::sin(angle_rand)};
}
dx /= length;
dy /= length;
// 4. Añadir variació aleatòria pequeña (±15°) per varietat visual
float angle_variacio =
((std::rand() % 30) - 15) * Defaults::Math::PI / 180.0F;
float cos_v = std::cos(angle_variacio);
float sin_v = std::sin(angle_variacio);
float final_x = (dx * cos_v) - (dy * sin_v);
float final_y = (dx * sin_v) + (dy * cos_v);
return {.x = final_x, .y = final_y};
}
void DebrisManager::reset() {
for (auto& debris : debris_pool_) {
debris.active = false;
}
}
auto DebrisManager::getActiveCount() const -> int {
int count = 0;
for (const auto& debris : debris_pool_) {
if (debris.active) {
count++;
}
}
return count;
}
} // namespace Effects
+20 -20
View File
@@ -3,7 +3,6 @@
#pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h>
#include <array>
@@ -13,15 +12,16 @@
#include "core/defaults.hpp"
#include "core/graphics/shape.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp"
#include "debris.hpp"
namespace Effects {
// Gestor de fragments de explosions
// Manté un pool de objectes Debris i gestiona el seu cicle de vida
class DebrisManager {
public:
// Gestor de fragments de explosions
// Manté un pool de objectes Debris i gestiona el seu cicle de vida
class DebrisManager {
public:
explicit DebrisManager(Rendering::Renderer* renderer);
// Crear explosión a partir de una shape
@@ -34,6 +34,9 @@ class DebrisManager {
// - velocitat_objecte: velocity de l'objecte que explota (px/s, per defecte 0)
// - velocitat_angular: velocity angular heretada (rad/s, per defecte 0)
// - factor_herencia_visual: factor de herència rotación visual (0.0-1.0, per defecte 0.0)
// - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s)
// - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60)
// - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar)
void explode(const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& centro,
float angle,
@@ -44,7 +47,10 @@ class DebrisManager {
float velocitat_angular = 0.0F,
float factor_herencia_visual = 0.0F,
const std::string& sound = Defaults::Sound::EXPLOSION,
SDL_Color color = {0, 0, 0, 0}); // alpha==0 → fragmentos usan oscilador global
SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global
float lifetime = Defaults::Physics::Debris::TEMPS_VIDA,
float friction = Defaults::Physics::Debris::ACCELERACIO,
int segment_multiplier = 1);
// Actualitzar todos los fragments active
void update(float delta_time);
@@ -58,14 +64,13 @@ class DebrisManager {
// Obtenir número de fragments active
[[nodiscard]] auto getActiveCount() const -> int;
private:
private:
Rendering::Renderer* renderer_;
// Pool de fragments (màxim concurrent)
// Un pentágono té 5 línies, 15 enemigos = 75 línies
// + ship (3 línies) + balas (5 línies * 3) = 93 línies màxim
// Arrodonit a 100 per seguretat
static constexpr int MAX_DEBRIS = 150;
// Pentàgon 5 línies × 15 enemics × multiplier 3 = 225 trossos només d'enemics.
// + ship (3 línies) + balas (5 línies × 3) = ~243. Arrodonit a 300.
static constexpr int MAX_DEBRIS = 300;
std::array<Debris, MAX_DEBRIS> debris_pool_;
// Trobar primer slot inactiu
@@ -81,14 +86,9 @@ class DebrisManager {
-> std::vector<std::pair<Vec2, Vec2>>;
// Inicialitza un debris en un slot lliure i el deixa actiu. Retorna
// false si el pool está ple (la cridadora ha d'aturar el bucle).
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2,
const Vec2& centro, float velocitat_base, float brightness,
const Vec2& velocitat_objecte, float velocitat_angular,
float factor_herencia_visual, SDL_Color color) -> bool;
static void applyAngularVelocity(Debris& debris, const Vec2& direccio,
float velocitat_angular);
static void applyVisualRotation(Debris& debris, float velocitat_angular,
float factor_herencia_visual);
};
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool;
static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular);
static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual);
};
} // namespace Effects
+1 -7
View File
@@ -16,12 +16,6 @@
#include "core/types.hpp"
#include "game/constants.hpp"
namespace {
// Velocidad escalar de las balas (px/s). Conserva el feel del Pascal original
// (7 px/frame × 20 FPS = 140 px/s).
constexpr float BULLET_SPEED = 140.0F;
} // namespace
Bullet::Bullet(Rendering::Renderer* renderer)
: Entity(renderer) {
// Brightness específico para balas
@@ -80,7 +74,7 @@ void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) {
body_.angle = angle;
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F));
body_.velocity = Vec2{.x = DIR_X * BULLET_SPEED, .y = DIR_Y * BULLET_SPEED};
body_.velocity = Vec2{.x = DIR_X * Defaults::Game::BULLET_SPEED, .y = DIR_Y * Defaults::Game::BULLET_SPEED};
body_.angular_velocity = 0.0F;
body_.clearAccumulators();
+70 -33
View File
@@ -41,16 +41,16 @@ namespace {
Enemy::Enemy(Rendering::Renderer* renderer)
: Entity(renderer),
tracking_strength_(0.5F) {
tracking_strength_(Defaults::Enemies::Cuadrado::TRACKING_STRENGTH) {
brightness_ = Defaults::Brightness::ENEMIC;
// Configuración del cuerpo físico — defaults para enemy genérico.
// init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo).
body_.setMass(5.0F); // Más liviano que la nave (10.0)
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
body_.restitution = 1.0F; // Rebote elástico perfecto contra paredes
body_.linear_damping = 0.0F; // Sin fricción: mantienen velocidad
body_.angular_damping = 0.0F; // Idem
body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS);
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
body_.restitution = Defaults::Enemies::Body::RESTITUTION;
body_.linear_damping = Defaults::Enemies::Body::LINEAR_DAMPING;
body_.angular_damping = Defaults::Enemies::Body::ANGULAR_DAMPING;
}
void Enemy::init(EnemyType type, const Vec2* ship_pos) {
@@ -60,7 +60,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
float base_speed = 0.0F;
float drotacio_min = 0.0F;
float drotacio_max = 0.0F;
float type_mass = 5.0F;
float type_mass = Defaults::Enemies::Body::DEFAULT_MASS;
switch (type_) {
case EnemyType::PENTAGON:
@@ -68,7 +68,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
base_speed = Defaults::Enemies::Pentagon::VELOCITAT;
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
type_mass = 5.0F;
type_mass = Defaults::Enemies::Pentagon::MASS;
break;
case EnemyType::QUADRAT:
@@ -76,7 +76,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
base_speed = Defaults::Enemies::Cuadrado::VELOCITAT;
drotacio_min = Defaults::Enemies::Cuadrado::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Cuadrado::DROTACIO_MAX;
type_mass = 8.0F; // Más pesado, "tanque"
type_mass = Defaults::Enemies::Cuadrado::MASS;
tracking_timer_ = 0.0F;
break;
@@ -85,7 +85,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
base_speed = Defaults::Enemies::Molinillo::VELOCITAT;
drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX;
type_mass = 4.0F; // Más liviano, ágil
type_mass = Defaults::Enemies::Molinillo::MASS;
break;
default:
@@ -153,7 +153,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
// Rotación visual aleatoria (independiente del body)
const float DROTACIO_RANGE = drotacio_max - drotacio_min;
drotacio_ = drotacio_min + ((static_cast<float>(std::rand()) / RAND_MAX) * DROTACIO_RANGE);
drotacio_ = drotacio_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DROTACIO_RANGE);
rotacio_ = 0.0F;
// Estado de animación
@@ -177,6 +177,17 @@ void Enemy::update(float delta_time) {
return;
}
// Decremento de timer "herido"; al cruzar 0 marca expiración para que el
// system layer dispare la explosión diferida.
wound_expired_this_frame_ = false;
if (wounded_timer_ > 0.0F) {
wounded_timer_ -= delta_time;
if (wounded_timer_ <= 0.0F) {
wounded_timer_ = 0.0F;
wound_expired_this_frame_ = true;
}
}
// Decremento de invulnerabilidad + LERP de brightness
if (timer_invulnerabilitat_ > 0.0F) {
timer_invulnerabilitat_ -= delta_time;
@@ -190,17 +201,21 @@ void Enemy::update(float delta_time) {
brightness_ = START + ((END - START) * SMOOTH_T);
}
// Comportamiento por tipo (ajusta body_.velocity, NO mueve posición)
switch (type_) {
case EnemyType::PENTAGON:
behaviorPentagon(delta_time);
break;
case EnemyType::QUADRAT:
behaviorQuadrat(delta_time);
break;
case EnemyType::MOLINILLO:
behaviorMolinillo(delta_time);
break;
// Comportamiento por tipo (ajusta body_.velocity, NO mueve posición).
// Skip cuando está herido: el enemy és un "cos mort" inert, només
// respon a la inèrcia del impulse rebut i a les col·lisions físiques.
if (!isWounded()) {
switch (type_) {
case EnemyType::PENTAGON:
behaviorPentagon(delta_time);
break;
case EnemyType::QUADRAT:
behaviorQuadrat(delta_time);
break;
case EnemyType::MOLINILLO:
behaviorMolinillo(delta_time);
break;
}
}
// Animaciones (palpitación + rotación acelerada)
@@ -234,6 +249,17 @@ void Enemy::draw() const {
color = Defaults::Palette::MOLINILLO;
break;
}
// Parpadeo dorado mientras está herido: alterna color de tipo ↔ dorado
// a Wounded::BLINK_HZ usando el timer (fmod sobre el periodo).
if (wounded_timer_ > 0.0F) {
const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ;
const float T = std::fmod(wounded_timer_, CYCLE);
if (T < (CYCLE / 2.0F)) {
color = Defaults::Palette::WOUNDED;
}
}
Rendering::renderShape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_, color);
}
@@ -242,6 +268,18 @@ void Enemy::destruir() {
body_.velocity = Vec2{};
body_.angular_velocity = 0.0F;
body_.radius = 0.0F; // No colisiona mientras está inactivo
wounded_timer_ = 0.0F;
wound_expired_this_frame_ = false;
last_hit_by_ = 0xFF;
}
void Enemy::herir(uint8_t shooter_id) {
wounded_timer_ = Defaults::Enemies::Wounded::DURATION;
last_hit_by_ = shooter_id;
}
void Enemy::applyImpulse(const Vec2& impulse) {
body_.applyImpulse(impulse);
}
void Enemy::setVelocity(float speed) {
@@ -268,11 +306,10 @@ void Enemy::behaviorPentagon(float delta_time) {
// Probabilidad de zigzag por segundo (calibrada para sensación equivalente
// a la versión vieja que disparaba en cada toque de pared).
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F;
const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
if (RAND_VAL < ZIGZAG_PROB_PER_SECOND * delta_time) {
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
if (RAND_VAL < Defaults::Enemies::Pentagon::ZIGZAG_PROB_PER_SECOND * delta_time) {
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
const float DELTA = (static_cast<float>(std::rand()) / RAND_MAX) *
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) *
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX;
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
const float SPEED = body_.velocity.length();
@@ -339,7 +376,7 @@ void Enemy::updatePalpitation(float delta_time) {
animacio_.palpitacio_activa = false;
}
} else {
const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
const float TRIGGER_PROB = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time;
if (RAND_VAL < TRIGGER_PROB) {
animacio_.palpitacio_activa = true;
@@ -348,17 +385,17 @@ void Enemy::updatePalpitation(float delta_time) {
const float FREQ_RANGE = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX -
Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN;
animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * FREQ_RANGE);
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * FREQ_RANGE);
const float AMP_RANGE = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX -
Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN;
animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * AMP_RANGE);
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * AMP_RANGE);
const float DUR_RANGE = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX -
Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN;
animacio_.palpitacio_temps_restant = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
}
}
}
@@ -378,7 +415,7 @@ void Enemy::updateRotationAcceleration(float delta_time) {
drotacio_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
}
} else {
const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time;
if (RAND_VAL < TRIGGER_PROB) {
animacio_.drotacio_t = 0.0F;
@@ -386,13 +423,13 @@ void Enemy::updateRotationAcceleration(float delta_time) {
const float MULT_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN;
const float MULTIPLIER = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * MULT_RANGE);
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * MULT_RANGE);
animacio_.drotacio_objetivo = animacio_.drotacio_base * MULTIPLIER;
const float DUR_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN;
animacio_.drotacio_duracio = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
}
}
}
+98 -81
View File
@@ -19,104 +19,121 @@ enum class EnemyType : uint8_t {
// Estado de animación (palpitación + rotación acelerada)
struct EnemyAnimation {
// Palpitación (efecto respiración)
bool palpitacio_activa = false;
float palpitacio_fase = 0.0F;
float palpitacio_frequencia = 2.0F;
float palpitacio_amplitud = 0.15F;
float palpitacio_temps_restant = 0.0F;
// Palpitación (efecto respiración)
bool palpitacio_activa = false;
float palpitacio_fase = 0.0F;
float palpitacio_frequencia = 2.0F;
float palpitacio_amplitud = 0.15F;
float palpitacio_temps_restant = 0.0F;
// Aceleración de rotación visual (modulación a largo plazo)
float drotacio_base = 0.0F;
float drotacio_objetivo = 0.0F;
float drotacio_t = 0.0F;
float drotacio_duracio = 0.0F;
// Aceleración de rotación visual (modulación a largo plazo)
float drotacio_base = 0.0F;
float drotacio_objetivo = 0.0F;
float drotacio_t = 0.0F;
float drotacio_duracio = 0.0F;
};
class Enemy : public Entities::Entity {
public:
Enemy()
: Entity(nullptr) {}
explicit Enemy(Rendering::Renderer* renderer);
public:
Enemy()
: Entity(nullptr) {}
explicit Enemy(Rendering::Renderer* renderer);
void init() override { init(EnemyType::PENTAGON, nullptr); }
void init(EnemyType type, const Vec2* ship_pos = nullptr);
void update(float delta_time) override;
void postUpdate(float delta_time) override;
void draw() const override;
void init() override { init(EnemyType::PENTAGON, nullptr); }
void init(EnemyType type, const Vec2* ship_pos = nullptr);
void update(float delta_time) override;
void postUpdate(float delta_time) override;
void draw() const override;
// Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return esta_; }
// Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return esta_; }
// Override: Interfaz de colisión
[[nodiscard]] auto getCollisionRadius() const -> float override {
return Defaults::Entities::ENEMY_RADIUS;
}
[[nodiscard]] auto isCollidable() const -> bool override {
return esta_ && timer_invulnerabilitat_ <= 0.0F;
}
// Override: Interfaz de colisión
[[nodiscard]] auto getCollisionRadius() const -> float override {
return Defaults::Entities::ENEMY_RADIUS;
}
[[nodiscard]] auto isCollidable() const -> bool override {
return esta_ && timer_invulnerabilitat_ <= 0.0F;
}
// Marcar destruido (desactiva el cuerpo físicamente: radius=0)
void destruir();
// Marcar destruido (desactiva el cuerpo físicamente: radius=0)
void destruir();
// Getters
[[nodiscard]] auto getRotationDelta() const -> float { return drotacio_; }
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
// Getters
[[nodiscard]] auto getRotationDelta() const -> float { return drotacio_; }
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
// Set ship position reference for tracking behavior
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
// Set ship position reference for tracking behavior
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
// Stage system API (base stats)
[[nodiscard]] auto getBaseVelocity() const -> float;
[[nodiscard]] auto getBaseRotation() const -> float;
[[nodiscard]] auto getType() const -> EnemyType { return type_; }
// Stage system API (base stats)
[[nodiscard]] auto getBaseVelocity() const -> float;
[[nodiscard]] auto getBaseRotation() const -> float;
[[nodiscard]] auto getType() const -> EnemyType { return type_; }
// Setters para multiplicadores de dificultad (stage system).
// Establecen la velocidad escalar deseada manteniendo la dirección
// actual del body_.velocity.
void setVelocity(float speed);
void setRotation(float rot) {
drotacio_ = rot;
animacio_.drotacio_base = rot;
}
void setTrackingStrength(float strength);
// Setters para multiplicadores de dificultad (stage system).
// Establecen la velocidad escalar deseada manteniendo la dirección
// actual del body_.velocity.
void setVelocity(float speed);
void setRotation(float rot) {
drotacio_ = rot;
animacio_.drotacio_base = rot;
}
void setTrackingStrength(float strength);
// Invulnerabilidad
[[nodiscard]] auto isInvulnerable() const -> bool { return timer_invulnerabilitat_ > 0.0F; }
[[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; }
// Invulnerabilidad
[[nodiscard]] auto isInvulnerable() const -> bool { return timer_invulnerabilitat_ > 0.0F; }
[[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; }
private:
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo.
float drotacio_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
float rotacio_{0.0F}; // Rotación visual acumulada (no afecta movimiento)
bool esta_{false};
// Estado "herido": entre primer impacto de bala y explosión diferida.
// shooter_id: id del jugador que herí; 0xFF = sin atribución (cadena, etc.).
void herir(uint8_t shooter_id = 0xFF);
[[nodiscard]] auto isWounded() const -> bool { return wounded_timer_ > 0.0F; }
[[nodiscard]] auto getWoundedTimer() const -> float { return wounded_timer_; }
[[nodiscard]] auto woundExpiredThisFrame() const -> bool { return wound_expired_this_frame_; }
void consumeWoundExpired() { wound_expired_this_frame_ = false; }
[[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; }
EnemyType type_{EnemyType::PENTAGON};
EnemyAnimation animacio_;
// Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico.
void applyImpulse(const Vec2& impulse);
// Comportamiento type-specific
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección
const Vec2* ship_position_{nullptr}; // Puntero a posición de la nave (para tracking)
float tracking_strength_{0.0F}; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección
private:
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo.
float drotacio_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
float rotacio_{0.0F}; // Rotación visual acumulada (no afecta movimiento)
bool esta_{false};
// Invulnerabilidad post-spawn
float timer_invulnerabilitat_{0.0F};
EnemyType type_{EnemyType::PENTAGON};
EnemyAnimation animacio_;
// Métodos privados
void updateAnimation(float delta_time);
void updatePalpitation(float delta_time);
void updateRotationAcceleration(float delta_time);
void behaviorPentagon(float delta_time);
void behaviorQuadrat(float delta_time);
void behaviorMolinillo(float delta_time);
[[nodiscard]] auto computeCurrentScale() const -> float;
// Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy.
static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool;
// Comportamiento type-specific
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección
const Vec2* ship_position_{nullptr}; // Puntero a posición de la nave (para tracking)
float tracking_strength_{0.0F}; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección
// Helper: setear body_.velocity desde un ángulo y magnitud.
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
void setVelocityFromAngle(float angle_movement, float speed);
// Invulnerabilidad post-spawn
float timer_invulnerabilitat_{0.0F};
// Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración.
float wounded_timer_{0.0F};
bool wound_expired_this_frame_{false};
uint8_t last_hit_by_{0xFF}; // 0xFF = sin atribución
// Métodos privados
void updateAnimation(float delta_time);
void updatePalpitation(float delta_time);
void updateRotationAcceleration(float delta_time);
void behaviorPentagon(float delta_time);
void behaviorQuadrat(float delta_time);
void behaviorMolinillo(float delta_time);
[[nodiscard]] auto computeCurrentScale() const -> float;
// Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy.
static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool;
// Helper: setear body_.velocity desde un ángulo y magnitud.
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
void setVelocityFromAngle(float angle_movement, float speed);
};
+7 -7
View File
@@ -25,11 +25,11 @@ Ship::Ship(Rendering::Renderer* renderer, const char* shape_file)
brightness_ = Defaults::Brightness::NAU;
// Configuración del cuerpo físico
body_.setMass(10.0F); // Masa de referencia para choques
body_.radius = Defaults::Entities::SHIP_RADIUS; // Radio de colisión
body_.restitution = 0.6F; // Rebote moderado contra paredes
body_.linear_damping = 1.5F; // Fricción exponencial (s⁻¹)
body_.angular_damping = 0.0F; // La rotación es 100% por input, no inercial
body_.setMass(Defaults::Ship::MASS);
body_.radius = Defaults::Entities::SHIP_RADIUS;
body_.restitution = Defaults::Ship::RESTITUTION;
body_.linear_damping = Defaults::Ship::LINEAR_DAMPING;
body_.angular_damping = Defaults::Ship::ANGULAR_DAMPING;
// Cargar shape compartida desde archivo
shape_ = Graphics::ShapeLoader::load(shape_file);
@@ -154,8 +154,8 @@ void Ship::draw() const {
// Efecto visual de empuje: escala proporcional a la velocidad.
// 0..200 px/s → escala 1.0..1.5 (manteniendo la sensación del Pascal original).
const float SPEED = getSpeed();
const float VISUAL_PUSH = SPEED / 33.33F;
const float SCALE = 1.0F + (VISUAL_PUSH / 12.0F);
const float VISUAL_PUSH = SPEED / Defaults::Ship::VISUAL_PUSH_DIVISOR;
const float SCALE = 1.0F + (VISUAL_PUSH / Defaults::Ship::VISUAL_SCALE_DIVISOR);
Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, Defaults::Palette::SHIP);
}
+15 -14
View File
@@ -107,8 +107,8 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
} else {
// Jugador inactiu: marcar como a mort permanent
ships_[i].markHit();
hit_timer_per_player_[i] = 999.0F; // Valor sentinella (permanent inactiu)
lives_per_player_[i] = 0; // Sin vides
hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
lives_per_player_[i] = 0; // Sin vides
std::cout << "[GameScene] Jugador " << (i + 1) << " inactiu\n";
}
}
@@ -201,8 +201,8 @@ void GameScene::stepMidGameJoin() {
// Solo se permite join si hay al menos un jugador vivo (no se puede
// hacer join en pantalla vacía).
const bool ALGU_VIU =
(match_config_.jugador1_actiu && hit_timer_per_player_[0] != 999.0F) ||
(match_config_.jugador2_actiu && hit_timer_per_player_[1] != 999.0F);
(match_config_.jugador1_actiu && hit_timer_per_player_[0] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) ||
(match_config_.jugador2_actiu && hit_timer_per_player_[1] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER);
if (!ALGU_VIU) {
return;
}
@@ -211,7 +211,7 @@ void GameScene::stepMidGameJoin() {
for (uint8_t pid = 0; pid < 2; pid++) {
const bool ACTIU = (pid == 0) ? match_config_.jugador1_actiu
: match_config_.jugador2_actiu;
const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == 999.0F;
const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
if (ACTIU && !MUERTO_SIN_VIDAS) {
continue; // jugador ya está jugando
}
@@ -284,7 +284,7 @@ auto GameScene::stepGameOver(float delta_time) -> bool {
void GameScene::stepDeathSequence(float delta_time) {
bool algun_mort = false;
for (uint8_t i = 0; i < 2; i++) {
if (hit_timer_per_player_[i] <= 0.0F || hit_timer_per_player_[i] >= 999.0F) {
if (hit_timer_per_player_[i] <= 0.0F || hit_timer_per_player_[i] >= Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) {
continue;
}
algun_mort = true;
@@ -304,7 +304,7 @@ void GameScene::stepDeathSequence(float delta_time) {
}
// Sin vidas: marcar definitivamente muerto y comprobar transición a CONTINUE.
hit_timer_per_player_[i] = 999.0F;
hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
const bool P1_DEAD = !match_config_.jugador1_actiu || lives_per_player_[0] <= 0;
const bool P2_DEAD = !match_config_.jugador2_actiu || lives_per_player_[1] <= 0;
if (P1_DEAD && P2_DEAD) {
@@ -628,8 +628,9 @@ void GameScene::tocado(uint8_t player_id) {
const Vec2& ship_pos = ships_[player_id].getCenter();
float ship_angle = ships_[player_id].getAngle();
Vec2 vel_nau = ships_[player_id].getVelocityVector();
// Reduir a 80% la velocity heretada per la ship (més realista)
Vec2 vel_nau_80 = {.x = vel_nau.x * 0.8F, .y = vel_nau.y * 0.8F};
// Reduir la velocity heretada per la ship segons defaults (més realista)
constexpr float INHERIT = Defaults::Physics::Debris::SHIP_VELOCITY_INHERITANCE;
Vec2 vel_nau_80 = {.x = vel_nau.x * INHERIT, .y = vel_nau.y * INHERIT};
debris_manager_.explode(
ships_[player_id].getShape(), // Ship shape (3 lines)
@@ -646,7 +647,7 @@ void GameScene::tocado(uint8_t player_id) {
);
// Start death timer (non-zero to avoid re-triggering)
hit_timer_per_player_[player_id] = 0.001F;
hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
}
// Phase 2 is automatic (debris updates in update())
// Phase 3 is handled in update() when hit_timer_per_player_ >= DEATH_DURATION
@@ -674,8 +675,8 @@ void GameScene::drawScoreboard() {
std::string text = buildScoreboard();
// Parámetros de renderització
const float SCALE = 0.85F;
const float SPACING = 0.0F;
const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
// Calcular centro de la zona del marcador
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
@@ -828,8 +829,8 @@ void GameScene::fireBullet(uint8_t player_id) {
const Vec2& ship_centre = ships_[player_id].getCenter();
float ship_angle = ships_[player_id].getAngle();
constexpr float LOCAL_TIP_X = 0.0F;
constexpr float LOCAL_TIP_Y = -12.0F;
constexpr float LOCAL_TIP_X = Defaults::Hud::Tips::LOCAL_X;
constexpr float LOCAL_TIP_Y = Defaults::Hud::Tips::LOCAL_Y;
float cos_a = std::cos(ship_angle);
float sin_a = std::sin(ship_angle);
float tip_x = (LOCAL_TIP_X * cos_a) - (LOCAL_TIP_Y * sin_a) + ship_centre.x;
+127 -48
View File
@@ -10,9 +10,77 @@
namespace Systems::Collision {
namespace {
constexpr uint8_t NO_SHOOTER = 0xFF;
// Lookup tabla puntos / color por tipo de enemy (mantiene la lógica
// anterior pero centralizada para reutilizar entre paths).
auto scoreForType(EnemyType type) -> int {
switch (type) {
case EnemyType::PENTAGON:
return Defaults::Enemies::Scoring::PENTAGON_SCORE;
case EnemyType::QUADRAT:
return Defaults::Enemies::Scoring::QUADRAT_SCORE;
case EnemyType::MOLINILLO:
return Defaults::Enemies::Scoring::MOLINILLO_SCORE;
}
return 0;
}
auto colorForType(EnemyType type) -> SDL_Color {
switch (type) {
case EnemyType::PENTAGON:
return Defaults::Palette::PENTAGON;
case EnemyType::QUADRAT:
return Defaults::Palette::QUADRAT;
case EnemyType::MOLINILLO:
return Defaults::Palette::MOLINILLO;
}
return SDL_Color{};
}
// Mata al enemy con explosión: floating score, debris con velocity heredada,
// sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador.
// CRUCIAL: leer velocity/datos ANTES de destruir() (que zera la velocity).
void explodeNow(Context& ctx, Enemy& enemy, uint8_t shooter_id) {
const Vec2 ENEMY_POS = enemy.getCenter();
const Vec2 ENEMY_VEL = enemy.getVelocityVector();
const float BRIGHTNESS = enemy.getBrightness();
const auto SHAPE = enemy.getShape();
const EnemyType TYPE = enemy.getType();
const int POINTS = scoreForType(TYPE);
const SDL_Color COLOR = colorForType(TYPE);
if (shooter_id != NO_SHOOTER) {
ctx.score_per_player[shooter_id] += POINTS;
}
ctx.floating_score_manager.crear(POINTS, ENEMY_POS);
enemy.destruir();
constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suave)
const Vec2 INHERITED_VEL = ENEMY_VEL * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
ctx.debris_manager.explode(
SHAPE,
ENEMY_POS,
0.0F, // angle (rotación interna del enemy)
1.0F, // escala
VELOCITAT_EXPLOSIO,
BRIGHTNESS,
INHERITED_VEL,
0.0F, // sense herència angular: evita que els 5 trossos curvin en bloc
0.0F, // sin herencia visual
Defaults::Sound::EXPLOSION,
COLOR,
Defaults::Physics::Debris::ENEMY_LIFETIME,
Defaults::Physics::Debris::ENEMY_FRICTION,
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
}
} // anonymous namespace
void detectBulletEnemy(Context& ctx) {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suau)
for (auto& bullet : ctx.bullets) {
for (auto& enemy : ctx.enemies) {
@@ -21,57 +89,66 @@ namespace Systems::Collision {
}
// *** COLISIÓN bullet → enemy ***
const Vec2& enemy_pos = enemy.getCenter();
// Empuje físico cuasi-realista: el impulse és el moment de la bala
// (m·v) multiplicat pel factor de transferència. Direcció = vector
// velocity de la bala (cap a on viatjava).
const Vec2 IMPULSE = bullet.getBody().velocity *
(bullet.getBody().mass * Defaults::Physics::Bullet::IMPACT_MOMENTUM_FACTOR);
enemy.applyImpulse(IMPULSE);
// 1. Puntos según tipo
int points = 0;
switch (enemy.getType()) {
case EnemyType::PENTAGON:
points = Defaults::Enemies::Scoring::PENTAGON_SCORE;
break;
case EnemyType::QUADRAT:
points = Defaults::Enemies::Scoring::QUADRAT_SCORE;
break;
case EnemyType::MOLINILLO:
points = Defaults::Enemies::Scoring::MOLINILLO_SCORE;
break;
const uint8_t SHOOTER = bullet.getOwnerId();
if (enemy.isWounded()) {
// Segundo impacto sobre enemy ya herido → muerte instantánea,
// puntos al nuevo shooter.
explodeNow(ctx, enemy, SHOOTER);
} else {
// Primer impacto → entra en estado herido (explosión diferida).
enemy.herir(SHOOTER);
}
uint8_t owner_id = bullet.getOwnerId();
ctx.score_per_player[owner_id] += points;
ctx.floating_score_manager.crear(points, enemy_pos);
// 2. Destruir enemy + crear explosión (debris hereda color del enemy)
SDL_Color enemy_color{};
switch (enemy.getType()) {
case EnemyType::PENTAGON:
enemy_color = Defaults::Palette::PENTAGON;
break;
case EnemyType::QUADRAT:
enemy_color = Defaults::Palette::QUADRAT;
break;
case EnemyType::MOLINILLO:
enemy_color = Defaults::Palette::MOLINILLO;
break;
}
enemy.destruir();
Vec2 vel_enemic = enemy.getVelocityVector();
ctx.debris_manager.explode(
enemy.getShape(),
enemy_pos,
0.0F, // angle (la rotación es interna del enemy)
1.0F, // escala
VELOCITAT_EXPLOSIO,
enemy.getBrightness(),
vel_enemic,
enemy.getRotationDelta(),
0.0F, // sin herencia visual
Defaults::Sound::EXPLOSION,
enemy_color);
// 3. Desactivar bullet (solo destruye 1 enemy)
bullet.desactivar();
break;
break; // Una bala impacta a un enemy y muere
}
}
}
void processWoundedDeaths(Context& ctx) {
for (auto& enemy : ctx.enemies) {
if (!enemy.woundExpiredThisFrame()) {
continue;
}
enemy.consumeWoundExpired();
explodeNow(ctx, enemy, enemy.getLastHitBy());
}
}
void detectWoundedChain(Context& ctx) {
const std::size_t N = ctx.enemies.size();
for (std::size_t i = 0; i < N; i++) {
Enemy& a = ctx.enemies[i];
if (!a.isCollidable()) {
continue;
}
for (std::size_t j = i + 1; j < N; j++) {
Enemy& b = ctx.enemies[j];
if (!b.isCollidable()) {
continue;
}
const bool A_WOUNDED = a.isWounded();
const bool B_WOUNDED = b.isWounded();
if (A_WOUNDED == B_WOUNDED) {
continue; // ambos sanos o ambos heridos: nada que propagar
}
if (!Physics::checkCollision(a, b, 1.0F)) {
continue;
}
// El sano queda herido, propagando el shooter original.
if (A_WOUNDED) {
b.herir(a.getLastHitBy());
} else {
a.herir(b.getLastHitBy());
}
}
}
}
@@ -147,7 +224,9 @@ namespace Systems::Collision {
}
void detectAll(Context& ctx) {
processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame
detectBulletEnemy(ctx);
detectWoundedChain(ctx); // un herit pot ferir a un sa al fregar-lo
detectShipEnemy(ctx);
detectBulletPlayer(ctx);
}
+26 -18
View File
@@ -26,9 +26,9 @@
namespace Systems::Collision {
// Todo lo que las detecciones necesitan leer/modificar. Vive en GameScene;
// se le pasa por referencia (no copia, no ownership).
struct Context {
// Todo lo que las detecciones necesitan leer/modificar. Vive en GameScene;
// se le pasa por referencia (no copia, no ownership).
struct Context {
std::array<Ship, 2>& ships;
std::array<Enemy, Defaults::Entities::MAX_ORNIS>& enemies;
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BALES) * 2>& bullets;
@@ -40,24 +40,32 @@ struct Context {
const GameConfig::MatchConfig& match_config;
// Trigger de muerte del jugador (GameScene::tocado).
std::function<void(uint8_t /*player_id*/)> on_player_hit;
};
};
// Detecta colisiones bullet → enemy. Si hit:
// - destruye el enemy (radius=0 en physics body)
// - crea debris + floating score
// - desactiva la bullet
// - suma puntos al shooter
void detectBulletEnemy(Context& ctx);
// Detecta colisiones bullet → enemy. Si hit:
// - Primer impacto: aplica impulse, marca al enemy como "herido", desactiva bullet.
// - Segundo impacto (enemy ya herido): explosión inmediata + puntos al shooter.
void detectBulletEnemy(Context& ctx);
// Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id).
void detectShipEnemy(Context& ctx);
// Procesa enemigos cuyo wound timer ha expirado este frame: explosión + puntos
// al `last_hit_by_` del enemy (si está set).
void processWoundedDeaths(Context& ctx);
// Detecta colisiones bullet → player (friendly fire / self-hit).
// Self-hit: el shooter pierde 1 vida. Teammate-hit: la víctima pierde 1, el
// atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet.
void detectBulletPlayer(Context& ctx);
// Si un enemy herido colisiona con uno sano (ni herido ni invulnerable),
// el sano también queda herido (efecto cadena). Propaga `last_hit_by_` para
// que el shooter original siga acreditándose la muerte en cascada. El rebote
// físico ya lo resuelve PhysicsWorld; aquí solo propagamos el estado.
void detectWoundedChain(Context& ctx);
// Las tres en orden lógico del frame.
void detectAll(Context& ctx);
// Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id).
void detectShipEnemy(Context& ctx);
// Detecta colisiones bullet → player (friendly fire / self-hit).
// Self-hit: el shooter pierde 1 vida. Teammate-hit: la víctima pierde 1, el
// atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet.
void detectBulletPlayer(Context& ctx);
// Las tres en orden lógico del frame.
void detectAll(Context& ctx);
} // namespace Systems::Collision
+75 -74
View File
@@ -13,87 +13,88 @@
namespace Systems::InitHud {
auto computeRangeProgress(float global_progress,
float ratio_init,
float ratio_end) -> float {
if (ratio_init >= ratio_end) {
return (global_progress >= ratio_end) ? 1.0F : 0.0F;
}
if (global_progress < ratio_init) {
return 0.0F;
}
if (global_progress > ratio_end) {
return 1.0F;
}
return (global_progress - ratio_init) / (ratio_end - ratio_init);
}
auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2 {
const float EASED = Easing::easeOutQuad(progress);
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
// Y inicial: 50 px bajo la zona de juego.
const float Y_INI = zone.y + zone.h + 50.0F;
const float Y_ANIM = Y_INI + ((final_position.y - Y_INI) * EASED);
return Vec2{.x = final_position.x, .y = Y_ANIM};
}
void drawBordersAnimated(Rendering::Renderer* renderer, float progress) {
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const float EASED = Easing::easeOutQuad(progress);
const int X1 = static_cast<int>(zone.x);
const int Y1 = static_cast<int>(zone.y);
const int X2 = static_cast<int>(zone.x + zone.w);
const int Y2 = static_cast<int>(zone.y + zone.h);
const int CX = (X1 + X2) / 2;
constexpr float PHASE_1_END = 0.33F;
constexpr float PHASE_2_END = 0.66F;
// Fase 1: línea superior crece desde el centro hacia los lados.
if (EASED > 0.0F) {
const float P = std::min(EASED / PHASE_1_END, 1.0F);
const int X_LEFT = static_cast<int>(CX - ((CX - X1) * P));
const int X_RIGHT = static_cast<int>(CX + ((X2 - CX) * P));
Rendering::linea(renderer, CX, Y1, X_LEFT, Y1);
Rendering::linea(renderer, CX, Y1, X_RIGHT, Y1);
auto computeRangeProgress(float global_progress,
float ratio_init,
float ratio_end) -> float {
if (ratio_init >= ratio_end) {
return (global_progress >= ratio_end) ? 1.0F : 0.0F;
}
if (global_progress < ratio_init) {
return 0.0F;
}
if (global_progress > ratio_end) {
return 1.0F;
}
return (global_progress - ratio_init) / (ratio_end - ratio_init);
}
// Fase 2: laterales bajan.
if (EASED > PHASE_1_END) {
const float P = std::min((EASED - PHASE_1_END) / (PHASE_2_END - PHASE_1_END), 1.0F);
const int Y_BOTTOM = static_cast<int>(Y1 + ((Y2 - Y1) * P));
Rendering::linea(renderer, X1, Y1, X1, Y_BOTTOM);
Rendering::linea(renderer, X2, Y1, X2, Y_BOTTOM);
auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2 {
const float EASED = Easing::easeOutQuad(progress);
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
// Y inicial: bajo la zona de juego (sale desde fuera).
const float Y_INI = zone.y + zone.h + Defaults::Hud::InitAnim::SHIP_SPAWN_Y_OFFSET;
const float Y_ANIM = Y_INI + ((final_position.y - Y_INI) * EASED);
return Vec2{.x = final_position.x, .y = Y_ANIM};
}
// Fase 3: línea inferior crece desde los lados hacia el centro.
if (EASED > PHASE_2_END) {
const float P = (EASED - PHASE_2_END) / (1.0F - PHASE_2_END);
const int X_LEFT = static_cast<int>(X1 + ((CX - X1) * P));
const int X_RIGHT = static_cast<int>(X2 - ((X2 - CX) * P));
Rendering::linea(renderer, X1, Y2, X_LEFT, Y2);
Rendering::linea(renderer, X2, Y2, X_RIGHT, Y2);
void drawBordersAnimated(Rendering::Renderer* renderer, float progress) {
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const float EASED = Easing::easeOutQuad(progress);
const int X1 = static_cast<int>(zone.x);
const int Y1 = static_cast<int>(zone.y);
const int X2 = static_cast<int>(zone.x + zone.w);
const int Y2 = static_cast<int>(zone.y + zone.h);
const int CX = (X1 + X2) / 2;
constexpr float PHASE_1_END = Defaults::Hud::InitAnim::BORDER_PHASE_1_END;
constexpr float PHASE_2_END = Defaults::Hud::InitAnim::BORDER_PHASE_2_END;
// Fase 1: línea superior crece desde el centro hacia los lados.
if (EASED > 0.0F) {
const float P = std::min(EASED / PHASE_1_END, 1.0F);
const int X_LEFT = static_cast<int>(CX - ((CX - X1) * P));
const int X_RIGHT = static_cast<int>(CX + ((X2 - CX) * P));
Rendering::linea(renderer, CX, Y1, X_LEFT, Y1);
Rendering::linea(renderer, CX, Y1, X_RIGHT, Y1);
}
// Fase 2: laterales bajan.
if (EASED > PHASE_1_END) {
const float P = std::min((EASED - PHASE_1_END) / (PHASE_2_END - PHASE_1_END), 1.0F);
const int Y_BOTTOM = static_cast<int>(Y1 + ((Y2 - Y1) * P));
Rendering::linea(renderer, X1, Y1, X1, Y_BOTTOM);
Rendering::linea(renderer, X2, Y1, X2, Y_BOTTOM);
}
// Fase 3: línea inferior crece desde los lados hacia el centro.
if (EASED > PHASE_2_END) {
const float P = (EASED - PHASE_2_END) / (1.0F - PHASE_2_END);
const int X_LEFT = static_cast<int>(X1 + ((CX - X1) * P));
const int X_RIGHT = static_cast<int>(X2 - ((X2 - CX) * P));
Rendering::linea(renderer, X1, Y2, X_LEFT, Y2);
Rendering::linea(renderer, X2, Y2, X_RIGHT, Y2);
}
}
}
void drawScoreboardAnimated(const Graphics::VectorText& text,
const std::string& scoreboard_text,
float progress) {
const float EASED = Easing::easeOutQuad(progress);
void drawScoreboardAnimated(const Graphics::VectorText& text,
const std::string& scoreboard_text,
float progress) {
const float EASED = Easing::easeOutQuad(progress);
constexpr float SCALE = 0.85F;
constexpr float SPACING = 0.0F;
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
const float CENTRE_X = scoreboard_zone.w / 2.0F;
const float Y_FINAL = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
// Posición inicial: fuera de la pantalla por debajo.
const auto Y_INI = static_cast<float>(Defaults::Game::HEIGHT);
const float Y_ANIM = Y_INI + ((Y_FINAL - Y_INI) * EASED);
constexpr float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
constexpr float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
const float CENTRE_X = scoreboard_zone.w / 2.0F;
const float Y_FINAL = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
// Posición inicial: fuera de la pantalla por debajo.
const auto Y_INI = static_cast<float>(Defaults::Game::HEIGHT);
const float Y_ANIM = Y_INI + ((Y_FINAL - Y_INI) * EASED);
text.renderCentered(scoreboard_text,
Vec2{.x = CENTRE_X, .y = Y_ANIM},
SCALE, SPACING);
}
text.renderCentered(scoreboard_text,
Vec2{.x = CENTRE_X, .y = Y_ANIM},
SCALE,
SPACING);
}
} // namespace Systems::InitHud
+99
View File
@@ -0,0 +1,99 @@
# compile_spirv.cmake
# Compila shaders GLSL a SPIR-V i genera headers C++ embedibles.
# Multiplataforma: Windows, macOS, Linux (no requereix bash ni xxd).
#
# Invocat per CMakeLists.txt amb:
# cmake -D GLSLC=<path> -D SHADERS_DIR=<path> -D HEADERS_DIR=<path> -P compile_spirv.cmake
#
# També es pot executar manualment des de l'arrel del projecte:
# cmake -D GLSLC=glslc -D SHADERS_DIR=shaders \
# -D HEADERS_DIR=source/core/rendering/gpu/spv \
# -P tools/shaders/compile_spirv.cmake
cmake_minimum_required(VERSION 3.10)
cmake_policy(SET CMP0007 NEW)
# Llista de shaders a compilar: font relativa a SHADERS_DIR
set(SHADER_SOURCES
"line.vert.glsl"
"line.frag.glsl"
"postfx.vert.glsl"
"postfx.frag.glsl"
)
# Nom de la variable C++ per a cada shader (mateix ordre).
# UPPER_CASE perquè són constexpr globals (.clang-tidy ho exigeix).
set(SHADER_VARS
"LINE_VERT_SPV"
"LINE_FRAG_SPV"
"POSTFX_VERT_SPV"
"POSTFX_FRAG_SPV"
)
# Flags extra per a cada shader (necessaris perquè .vert.glsl/.frag.glsl no s'infereixen)
set(SHADER_FLAGS
"-fshader-stage=vert"
"-fshader-stage=frag"
"-fshader-stage=vert"
"-fshader-stage=frag"
)
list(LENGTH SHADER_SOURCES NUM_SHADERS)
math(EXPR LAST_IDX "${NUM_SHADERS} - 1")
foreach(IDX RANGE ${LAST_IDX})
list(GET SHADER_SOURCES ${IDX} SRC_NAME)
list(GET SHADER_VARS ${IDX} VAR)
list(GET SHADER_FLAGS ${IDX} EXTRA_FLAG)
# Derivem el nom del header a partir de la variable: LINE_VERT_SPV line_vert_spv.h
string(TOLOWER "${VAR}" HDR_BASE)
set(SRC "${SHADERS_DIR}/${SRC_NAME}")
set(SPV "${HEADERS_DIR}/${HDR_BASE}.spv")
set(HDR "${HEADERS_DIR}/${HDR_BASE}.h")
message(STATUS "Compilant ${SRC} ...")
if(EXTRA_FLAG)
execute_process(
COMMAND "${GLSLC}" "${EXTRA_FLAG}" -O "${SRC}" -o "${SPV}"
RESULT_VARIABLE GLSLC_RESULT
ERROR_VARIABLE GLSLC_ERROR
)
else()
execute_process(
COMMAND "${GLSLC}" -O "${SRC}" -o "${SPV}"
RESULT_VARIABLE GLSLC_RESULT
ERROR_VARIABLE GLSLC_ERROR
)
endif()
if(NOT GLSLC_RESULT EQUAL 0)
message(FATAL_ERROR "glslc ha fallat per a ${SRC}:\n${GLSLC_ERROR}")
endif()
# Llegim el binari SPV com a hex (sense separadors) i el dividim en bytes.
file(READ "${SPV}" HEX_DATA HEX)
string(REGEX MATCHALL ".." BYTES "${HEX_DATA}")
list(LENGTH BYTES NUM_BYTES)
set(ARRAY_BODY "")
foreach(BYTE ${BYTES})
string(APPEND ARRAY_BODY " 0x${BYTE},\n")
endforeach()
file(WRITE "${HDR}"
"#pragma once\n"
"#include <cstddef>\n"
"#include <cstdint>\n"
"static const uint8_t ${VAR}[] = {\n"
"${ARRAY_BODY}"
"};\n"
"static const size_t ${VAR}_SIZE = ${NUM_BYTES};\n"
)
file(REMOVE "${SPV}")
message(STATUS " -> ${HDR} (${NUM_BYTES} bytes)")
endforeach()
message(STATUS "Shaders SPIR-V compilats correctament.")