From 4b0d85c010be48574f166262d12e7f0e3f1cbaf3 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 22:39:04 +0200 Subject: [PATCH 01/10] tweak(palette): colors neon purs per als 3 enemics (cyan/roig/magenta) --- source/core/defaults/palette.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/core/defaults/palette.hpp b/source/core/defaults/palette.hpp index 527337a..6aef5b7 100644 --- a/source/core/defaults/palette.hpp +++ b/source/core/defaults/palette.hpp @@ -14,11 +14,11 @@ namespace Defaults::Palette { // brillantor perceptual sota el bloom (sense alterar la identitat de color). // El canal dominant es manté a 255 a cada color per maximitzar la saturació // visible quan el halo s'expandeix. - constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro - constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser - constexpr SDL_Color PENTAGON = {.r = 155, .g = 195, .b = 255, .a = 255}; // Azul "esquivador" - constexpr SDL_Color QUADRAT = {.r = 255, .g = 140, .b = 140, .a = 255}; // Rojo "tank" - constexpr SDL_Color MOLINILLO = {.r = 255, .g = 160, .b = 255, .a = 255}; // Magenta agresivo - constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido + constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro + constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser + constexpr SDL_Color PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador" + constexpr SDL_Color QUADRAT = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank" + constexpr SDL_Color MOLINILLO = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu" + constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido } // namespace Defaults::Palette From 26bd5a9efa7c8cffbbd54a98290a4dc2f780a0af Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 22:43:32 +0200 Subject: [PATCH 02/10] tweak(playfield): el grid principal es dibuixa sobre el subgrid a les interseccions --- source/core/graphics/playfield.cpp | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/source/core/graphics/playfield.cpp b/source/core/graphics/playfield.cpp index 8d74d42..f382a4e 100644 --- a/source/core/graphics/playfield.cpp +++ b/source/core/graphics/playfield.cpp @@ -196,13 +196,39 @@ namespace Graphics { lines_.clear(); lines_.reserve(verticals.size() + horizontals.size()); + // El spawn_time_s s'assigna per índex espacial perquè la diagonal de + // l'ona de creixement avanci uniformement. L'ordre dins lines_, en + // canvi, ha de garantir que el grid principal (més brillant) es + // dibuixi DESPRÉS del subgrid: així a les interseccions guanya el + // principal i no queden tallades pel subgrid. for (int i = 0; i < NUM_V; i++) { verticals[i].spawn_time_s = static_cast(i) * INTERVAL_V; - lines_.push_back(verticals[i]); } for (int i = 0; i < NUM_H; i++) { horizontals[i].spawn_time_s = static_cast(i) * INTERVAL_H; - lines_.push_back(horizontals[i]); + } + + // Passada 1: subgrid (verticals + horitzontals). + for (const auto& v : verticals) { + if (v.brightness < Defaults::Playfield::GRID_BRIGHTNESS) { + lines_.push_back(v); + } + } + for (const auto& h : horizontals) { + if (h.brightness < Defaults::Playfield::GRID_BRIGHTNESS) { + lines_.push_back(h); + } + } + // Passada 2: grid principal (verticals + horitzontals). + for (const auto& v : verticals) { + if (v.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) { + lines_.push_back(v); + } + } + for (const auto& h : horizontals) { + if (h.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) { + lines_.push_back(h); + } } } From 1ea38d4f6aae2b2dad8e45f06012a41d011e9e86 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 22:47:02 +0200 Subject: [PATCH 03/10] =?UTF-8?q?fix(ship-death):=20debris=20hereten=20in?= =?UTF-8?q?=C3=A8rcia=20(captura=20velocitat=20abans=20del=20markHit)=20i?= =?UTF-8?q?=20comparteixen=20dispersi=C3=B3=20amb=20enemics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/scenes/game_scene.cpp | 44 ++++++++++++++++++------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index e0282df..76f1489 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -723,30 +723,36 @@ void GameScene::tocado(uint8_t player_id) { if (hit_timer_per_player_[player_id] == 0.0F) { // *** PHASE 1: TRIGGER DEATH *** + // Capturar velocitat ABANS del markHit (que la reseteja a zero). + // Sense això, els debris no hereten cap inèrcia de la nau. + const Vec2 SHIP_VEL_PRE_DEATH = ships_[player_id].getVelocityVector(); + const Vec2 SHIP_POS = ships_[player_id].getCenter(); + const float SHIP_ANGLE = ships_[player_id].getAngle(); + const float SHIP_BRIGHT = ships_[player_id].getBrightness(); + // Mark ship as dead (stops rendering and input) ships_[player_id].markHit(); - // Create ship explosion - const Vec2& ship_pos = ships_[player_id].getCenter(); - float ship_angle = ships_[player_id].getAngle(); - Vec2 vel_nau = ships_[player_id].getVelocityVector(); - // 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}; + const Vec2 INHERITED_VEL = SHIP_VEL_PRE_DEATH * + Defaults::Physics::Debris::SHIP_VELOCITY_INHERITANCE; + // Mateixa dispersió i efecte que els debris d'enemic (lifetime, + // friction, segment_multiplier alineats); només canvien sound i color. debris_manager_.explode( - ships_[player_id].getShape(), // Ship shape (3 lines) - ship_pos, // Center position - ship_angle, // Ship orientation - 1.0F, // Normal scale - Defaults::Physics::Debris::VELOCITAT_BASE, // 80 px/s - ships_[player_id].getBrightness(), // Heredar brightness - vel_nau_80, // Heredar 80% velocity - 0.0F, // Nave: trayectorias rectas (sin drotacio) - 0.0F, // Sin herencia visual (rotación aleatoria) - Defaults::Sound::EXPLOSION2, // Sonido alternativo para la explosión - Defaults::Palette::SHIP // Debris hereda color de la nave - ); + ships_[player_id].getShape(), + SHIP_POS, + SHIP_ANGLE, + 1.0F, + Defaults::Physics::Debris::VELOCITAT_BASE, + SHIP_BRIGHT, + INHERITED_VEL, + 0.0F, // sense herència angular + 0.0F, // sin herencia visual + Defaults::Sound::EXPLOSION2, + Defaults::Palette::SHIP, + Defaults::Physics::Debris::ENEMY_LIFETIME, + Defaults::Physics::Debris::ENEMY_FRICTION, + Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER); // Start death timer (non-zero to avoid re-triggering) hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH; From d86b10c14e557409fbe30f3a6279e623a5c02064 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 22:59:27 +0200 Subject: [PATCH 04/10] =?UTF-8?q?tweak(collision):=20impuls=20extra=20a=20?= =?UTF-8?q?l'enemic=20en=20el=20moment=20que=20mata=20la=20nau=20(factor?= =?UTF-8?q?=200.3=C2=B7mass=C2=B7vel)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/core/defaults/physics.hpp | 8 ++++++++ source/game/systems/collision_system.cpp | 17 +++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/source/core/defaults/physics.hpp b/source/core/defaults/physics.hpp index e3fbf4e..a515610 100644 --- a/source/core/defaults/physics.hpp +++ b/source/core/defaults/physics.hpp @@ -18,6 +18,14 @@ namespace Defaults::Physics { constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic } // namespace Bullet + // Ship → enemy: impuls explícit aplicat a l'enemic en el moment exacte + // que la nau mor per col·lisió amb ell (afegit per damunt del rebot + // natural de PhysicsWorld, que ja és present però subtil amb la + // damping de la nau). + namespace Ship { + constexpr float DEATH_IMPACT_MOMENTUM_FACTOR = 0.3F; + } // namespace Ship + // Explosions (debris physics) namespace Debris { constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s) diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index 4d91726..c300701 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -203,24 +203,29 @@ namespace Systems::Collision { } // Comprovem si la nau toca QUALSEVOL enemic vulnerable aquest frame. - bool touching_now = false; - for (const auto& enemy : ctx.enemies) { + Enemy* touched_enemy = nullptr; + for (auto& enemy : ctx.enemies) { if (enemy.isInvulnerable()) { continue; } if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) { - touching_now = true; + touched_enemy = &enemy; break; } } + const bool TOUCHING_NOW = touched_enemy != nullptr; // Edge-trigger: només compta com a impacte la transició no-tocant → tocant. // Així el contacte continu durant el rebot frame-a-frame no dispara HURT i mort // en frames consecutius. - const bool RISING_EDGE = touching_now && !ctx.ships[i].wasTouchingEnemyPrevFrame(); + const bool RISING_EDGE = TOUCHING_NOW && !ctx.ships[i].wasTouchingEnemyPrevFrame(); if (RISING_EDGE) { if (ctx.ships[i].isHurt()) { - // Segon impacte durant HURT → mort definitiva (mateix flux que abans). + // Segon impacte durant HURT → mort. Aplica un impuls afegit + // perquè l'enemic surti disparat (feedback visible). + const Vec2 SHIP_VEL = ctx.ships[i].getVelocityVector(); + const Vec2 IMPULSE = SHIP_VEL * (Defaults::Ship::MASS * Defaults::Physics::Ship::DEATH_IMPACT_MOMENTUM_FACTOR); + touched_enemy->applyImpulse(IMPULSE); ctx.on_player_hit(i); } else { // Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld; @@ -228,7 +233,7 @@ namespace Systems::Collision { ctx.ships[i].herir(); } } - ctx.ships[i].setTouchingEnemyPrevFrame(touching_now); + ctx.ships[i].setTouchingEnemyPrevFrame(TOUCHING_NOW); } } From b1ee23cd207d5022c3481a0e3ffe7238ed225d15 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 23:02:23 +0200 Subject: [PATCH 05/10] tweak(stage-messages): missatges level start/completed amb color ambre del PRESS START --- source/game/scenes/game_scene.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 76f1489..87cd6c0 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -879,9 +879,10 @@ void GameScene::drawStageMessage(const std::string& message) { float x = play_area.x + ((play_area.w - full_text_width) / 2.0F); float y = play_area.y + (play_area.h * Defaults::Game::STAGE_MESSAGE_Y_RATIO) - (text_height / 2.0F); - // Render only the partial message (typewriter effect) + // Render only the partial message (typewriter effect) amb el color + // ambre neon del "PRESS START" del títol — unifica el feel dels missatges. Vec2 pos = {.x = x, .y = y}; - text_.render(partial_message, pos, scale, SPACING); + text_.render(partial_message, pos, scale, SPACING, 1.0F, Defaults::Title::Colors::PRESS_START); } // ======================================== From e954d4ea59f4e4794d4357be86a592947304930e Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 23:10:06 +0200 Subject: [PATCH 06/10] =?UTF-8?q?tweak(playfield):=20rejilla=20violeta=20s?= =?UTF-8?q?ynthwave=20+=20brillos=20+5%;=20starfield=20unificat=20al=20col?= =?UTF-8?q?or=20del=20t=C3=ADtol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/core/defaults/playfield.hpp | 9 +++++++-- source/core/graphics/playfield.cpp | 12 +++++++++--- source/core/graphics/starfield_parallax.cpp | 11 +++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/source/core/defaults/playfield.hpp b/source/core/defaults/playfield.hpp index 512722c..d0dc67a 100644 --- a/source/core/defaults/playfield.hpp +++ b/source/core/defaults/playfield.hpp @@ -3,6 +3,8 @@ #pragma once +#include + namespace Defaults::Playfield { // Estructura de la graella (cel·les omplen tota la PLAYAREA) @@ -11,8 +13,11 @@ namespace Defaults::Playfield { constexpr int SUBDIVISIONS = 4; // cada cel·la principal es divideix en N subcel·les // Brillo respecte al color global (border = 1.0) - constexpr float GRID_BRIGHTNESS = 0.15F; - constexpr float SUBGRID_BRIGHTNESS = 0.05F; + constexpr float GRID_BRIGHTNESS = 0.20F; + constexpr float SUBGRID_BRIGHTNESS = 0.10F; + + // Color de la rejilla (lila/violeta synthwave). Es modula amb brillantor. + constexpr SDL_Color GRID_COLOR = {.r = 160, .g = 80, .b = 255, .a = 255}; // Animació de creació amb timer intern del Playfield. // L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en diff --git a/source/core/graphics/playfield.cpp b/source/core/graphics/playfield.cpp index f382a4e..4358cb5 100644 --- a/source/core/graphics/playfield.cpp +++ b/source/core/graphics/playfield.cpp @@ -290,7 +290,9 @@ namespace Graphics { static_cast(START_Y), static_cast(END_X), static_cast(END_Y), - line.brightness); + line.brightness, + 0.0F, + Defaults::Playfield::GRID_COLOR); // Cap brillant mentre creix. if (P < 1.0F) { const float LENGTH = std::sqrt((DX * DX) + (DY * DY)); @@ -302,7 +304,9 @@ namespace Graphics { static_cast(START_Y + (DY * HEAD_T)), static_cast(END_X), static_cast(END_Y), - Defaults::Playfield::HEAD_BRIGHTNESS); + Defaults::Playfield::HEAD_BRIGHTNESS, + 0.0F, + Defaults::Playfield::GRID_COLOR); } } return; @@ -329,7 +333,9 @@ namespace Graphics { static_cast(prev_y), static_cast(NX), static_cast(NY), - line.brightness); + line.brightness, + 0.0F, + Defaults::Playfield::GRID_COLOR); prev_x = NX; prev_y = NY; } diff --git a/source/core/graphics/starfield_parallax.cpp b/source/core/graphics/starfield_parallax.cpp index 32b4da3..8735ace 100644 --- a/source/core/graphics/starfield_parallax.cpp +++ b/source/core/graphics/starfield_parallax.cpp @@ -31,20 +31,15 @@ namespace Graphics { const float MIN_Y = zona.y; const float MAX_Y = zona.y + zona.h; - // Tint aleatori entre blanc (255,255,255) i cyan (0,255,255) per estrella. - // T ∈ [0,1]: 0 → blanc; 1 → cyan. R = 255·(1-T), G=B=255. + // Color únic per a totes les estrelles: el mateix blanc-blau gel + // del starfield del títol (Defaults::Title::Colors::STARFIELD). const auto FILL_LAYER = [&](int layer, int count, int& idx) { for (int i = 0; i < count; i++) { - const float T = randUniform(0.0F, 1.0F); stars_[idx++] = Star{ .x = randUniform(MIN_X, MAX_X), .y = randUniform(MIN_Y, MAX_Y), .layer = layer, - .color = SDL_Color{ - .r = static_cast(255.0F * (1.0F - T)), - .g = 255, - .b = 255, - .a = 255}}; + .color = Defaults::Title::Colors::STARFIELD}; } }; From d618b6d561484362c6d8db03766f52d5d7ce5618 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 23:20:18 +0200 Subject: [PATCH 07/10] feat(audio): so propi per a la nau a HURT (hurt.wav, separat del HIT de bala) --- data/sounds/effects/hurt.wav | Bin 0 -> 8812 bytes source/core/defaults/audio.hpp | 1 + source/game/entities/ship.cpp | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 data/sounds/effects/hurt.wav diff --git a/data/sounds/effects/hurt.wav b/data/sounds/effects/hurt.wav new file mode 100644 index 0000000000000000000000000000000000000000..f556023cf13ec1f7174b7a75aaae091141001571 GIT binary patch literal 8812 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 QyDl=c8m<_8jX|(~0vDd@Q2+n{ literal 0 HcmV?d00001 diff --git a/source/core/defaults/audio.hpp b/source/core/defaults/audio.hpp index 72230b3..84ea984 100644 --- a/source/core/defaults/audio.hpp +++ b/source/core/defaults/audio.hpp @@ -39,6 +39,7 @@ namespace Defaults::Sound { constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit constexpr const char* HIT = "effects/hit.wav"; // Enemic ferit (primer impacte → HURT) + constexpr const char* HURT = "effects/hurt.wav"; // Nau pròpia entra a HURT constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo constexpr const char* LOGO = "effects/logo.wav"; // Logo diff --git a/source/game/entities/ship.cpp b/source/game/entities/ship.cpp index 1cbde9a..996948d 100644 --- a/source/game/entities/ship.cpp +++ b/source/game/entities/ship.cpp @@ -182,5 +182,5 @@ void Ship::draw() const { void Ship::herir() { hurt_timer_ = Defaults::Ship::Hurt::DURATION; - Audio::get()->playSound(Defaults::Sound::HIT, Audio::Group::GAME); + Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME); } From 7e52eaeddb36660a13e653ebade4736822bebd5e Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 23:24:42 +0200 Subject: [PATCH 08/10] =?UTF-8?q?tweak(friendly-fire):=20la=20bala=20empen?= =?UTF-8?q?y=20la=20nau=20abans=20de=20morir=20=E2=86=92=20els=20debris=20?= =?UTF-8?q?hereten=20la=20in=C3=A8rcia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/systems/collision_system.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index c300701..207ef10 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -273,7 +273,12 @@ namespace Systems::Collision { } // *** TEAMMATE HIT (friendly fire) *** - // Víctima perd 1 vida, atacant en guanya 1. + // Víctima perd 1 vida, atacant en guanya 1. Apliquem l'impuls + // de la bala a la nau ABANS de on_player_hit perquè tocado() + // captura la velocitat per als debris (si no, queden quiets). + const Vec2 BULLET_IMPULSE = bullet.getBody().velocity * + (bullet.getBody().mass * Defaults::Physics::Bullet::IMPACT_MOMENTUM_FACTOR); + ctx.ships[player_id].getBody().applyImpulse(BULLET_IMPULSE); ctx.on_player_hit(player_id); ctx.lives_per_player[BULLET_OWNER]++; Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); From 70ca19eb8785177035b24e7ccf279d38bc4276b9 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 23:32:28 +0200 Subject: [PATCH 09/10] =?UTF-8?q?fix(wounded-chain):=20amplifier=201.25=20?= =?UTF-8?q?perqu=C3=A8=20la=20cadena=20agafi=20el=20contacte=20post-rebot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/core/defaults/game.hpp | 3 +++ source/game/systems/collision_system.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/source/core/defaults/game.hpp b/source/core/defaults/game.hpp index a98a675..5c08046 100644 --- a/source/core/defaults/game.hpp +++ b/source/core/defaults/game.hpp @@ -22,6 +22,9 @@ namespace Defaults::Game { // (1.05F) per tolerar floating-point i petites separacions post-impuls. constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 1.05F; constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous) + // Wounded chain: el rebot físic separa els cossos abans que arribi + // la detecció gameplay; amplier generós perquè el toc compti. + constexpr float COLLISION_WOUNDED_CHAIN_AMPLIFIER = 1.25F; // Friendly fire system constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index 207ef10..2185b35 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -177,7 +177,7 @@ namespace Systems::Collision { if (A_WOUNDED == B_WOUNDED) { continue; // ambos sanos o ambos heridos: nada que propagar } - if (!Physics::checkCollision(a, b, 1.0F)) { + if (!Physics::checkCollision(a, b, Defaults::Game::COLLISION_WOUNDED_CHAIN_AMPLIFIER)) { continue; } // El sano queda herido, propagando el shooter original. From 3b1e469a4f1898975226301ed3b9fb55c3903bb1 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 23:46:56 +0200 Subject: [PATCH 10/10] =?UTF-8?q?feat(title):=20destell=20hiperespacial=20?= =?UTF-8?q?al=20VP=20quan=20la=20nau=20desapareix=20(sparkle=204-puntes=20?= =?UTF-8?q?c=C3=B2ncau)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/shapes/title_flash.shp | 9 +++++ source/game/scenes/title_scene.cpp | 55 +++++++++++++++++++++++++++++ source/game/scenes/title_scene.hpp | 14 ++++++++ source/game/title/ship_animator.cpp | 7 +++- source/game/title/ship_animator.hpp | 7 ++++ 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 data/shapes/title_flash.shp diff --git a/data/shapes/title_flash.shp b/data/shapes/title_flash.shp new file mode 100644 index 0000000..cdcce3e --- /dev/null +++ b/data/shapes/title_flash.shp @@ -0,0 +1,9 @@ +# title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style) +# 4 puntes als cardinals (radi 30) i valls còncaus als 45° (corba Bezier +# quadràtica amb control point ±8). 5 punts per arc subdividint la corba. + +name: title_flash +scale: 1.0 +center: 0, 0 + +polyline: 0,-30 3.76,-21.76 8.64,-14.64 14.64,-8.64 21.76,-3.76 30,0 21.76,3.76 14.64,8.64 8.64,14.64 3.76,21.76 0,30 -3.76,21.76 -8.64,14.64 -14.64,8.64 -21.76,3.76 -30,0 -21.76,-3.76 -14.64,-8.64 -8.64,-14.64 -3.76,-21.76 0,-30 diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 4d0fd03..7012384 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -73,6 +73,15 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context) // correcte de la intro coreografiada (també quan venim de JUMP_TO_TITLE_MAIN). ship_animator_->setVisible(false); + // Flash que tapa el "pop" final de la nau al VP. Es spawneja al centre + // de pantalla (= projecció del VP) quan ship_animator avisa. + flash_shape_ = Graphics::ShapeLoader::load("title_flash.shp"); + ship_animator_->setOnShipDisappear([this](int /*player_id*/) { + triggerFlash(Vec2{ + .x = static_cast(Defaults::Window::WIDTH) / 2.0F, + .y = static_cast(Defaults::Window::HEIGHT) / 2.0F}); + }); + initTitle(); inicialitzarJailgames(); @@ -294,6 +303,7 @@ void TitleScene::update(float delta_time) { estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) { ship_animator_->update(delta_time); } + updateFlashes(delta_time); switch (estat_actual_) { case TitleState::STARFIELD_FADE_IN: @@ -524,6 +534,7 @@ void TitleScene::draw() { estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) { ship_animator_->draw(); } + drawFlashes(); if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) { return; @@ -629,3 +640,47 @@ auto TitleScene::checkStartGameButtonPressed() -> bool { void TitleScene::handleEvent(const SDL_Event& event) { (void)event; } + +namespace { + constexpr float FLASH_DURATION = 0.40F; + constexpr float FLASH_MAX_SCALE = 2.5F; + constexpr SDL_Color FLASH_COLOR = {.r = 255, .g = 255, .b = 255, .a = 255}; +} // namespace + +void TitleScene::triggerFlash(Vec2 pos) { + for (auto& f : flashes_) { + if (!f.active) { + f.active = true; + f.position = pos; + f.timer = 0.0F; + return; + } + } +} + +void TitleScene::updateFlashes(float delta_time) { + for (auto& f : flashes_) { + if (!f.active) { + continue; + } + f.timer += delta_time; + if (f.timer >= FLASH_DURATION) { + f.active = false; + } + } +} + +void TitleScene::drawFlashes() { + if (!flash_shape_) { + return; + } + for (const auto& f : flashes_) { + if (!f.active) { + continue; + } + // Escala 0 → max al midpoint → 0. Sinus simètric. + const float T_NORM = f.timer / FLASH_DURATION; + const float SCALE = FLASH_MAX_SCALE * std::sin(T_NORM * Defaults::Math::PI); + Rendering::renderShape(sdl_.getRenderer(), flash_shape_, f.position, 0.0F, SCALE, 1.0F, 1.0F, FLASH_COLOR); + } +} diff --git a/source/game/scenes/title_scene.hpp b/source/game/scenes/title_scene.hpp index 1191f46..6f03692 100644 --- a/source/game/scenes/title_scene.hpp +++ b/source/game/scenes/title_scene.hpp @@ -63,6 +63,20 @@ class TitleScene final : public Scene { std::unique_ptr camera_; std::unique_ptr starfield_; std::unique_ptr ship_animator_; + + // Destell que tapa el "pop" final de cada nau quan arriba al VP. + // Pool fix de 2 (una per nau). Anima escala 0→max→0. + struct Flash { + Vec2 position{}; + float timer{0.0F}; + bool active{false}; + }; + std::array flashes_{}; + std::shared_ptr flash_shape_; + + void triggerFlash(Vec2 pos); + void updateFlashes(float delta_time); + void drawFlashes(); TitleState estat_actual_{TitleState::STARFIELD_FADE_IN}; float temps_acumulat_{0.0F}; diff --git a/source/game/title/ship_animator.cpp b/source/game/title/ship_animator.cpp index a81acca..8400d48 100644 --- a/source/game/title/ship_animator.cpp +++ b/source/game/title/ship_animator.cpp @@ -142,9 +142,14 @@ namespace Title { case ShipState::FLOATING: updateFloating(ship, delta_time); break; - case ShipState::EXITING: + case ShipState::EXITING: { updateExiting(ship, delta_time); + // Transició a invisible: la nau acaba d'arribar al VP. + if (!ship.visible && on_ship_disappear_) { + on_ship_disappear_(ship.player_id); + } break; + } } } } diff --git a/source/game/title/ship_animator.hpp b/source/game/title/ship_animator.hpp index 24cc7fd..904e6f3 100644 --- a/source/game/title/ship_animator.hpp +++ b/source/game/title/ship_animator.hpp @@ -10,6 +10,7 @@ #include #include +#include #include "core/graphics/camera3d.hpp" #include "core/graphics/wireframe3d.hpp" @@ -70,10 +71,16 @@ namespace Title { [[nodiscard]] auto isAnimationComplete() const -> bool; [[nodiscard]] auto isVisible() const -> bool; + // Callback disparat quan una nau acaba l'EXITING (es torna invisible + // al VP). Útil per a un destell que tapi el pop final. + using ShipDisappearCallback = std::function; + void setOnShipDisappear(ShipDisappearCallback cb) { on_ship_disappear_ = std::move(cb); } + private: Rendering::Renderer* renderer_; const Graphics::Camera3D* camera_; std::array ships_; + ShipDisappearCallback on_ship_disappear_; static void updateEntering(TitleShip& ship, float delta_time); static void updateFloating(TitleShip& ship, float delta_time);