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/data/sounds/effects/hurt.wav b/data/sounds/effects/hurt.wav new file mode 100644 index 0000000..f556023 Binary files /dev/null and b/data/sounds/effects/hurt.wav differ 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/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/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 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/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 8d74d42..4358cb5 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); + } } } @@ -264,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)); @@ -276,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; @@ -303,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}; } }; 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); } diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index e0282df..87cd6c0 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; @@ -873,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); } // ======================================== 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/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index 4d91726..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. @@ -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); } } @@ -268,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); 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);