Merge branch 'tweak/misc-adjustments': retocs varis (paleta, glow, audio, física, destell del títol)

This commit is contained in:
2026-05-22 23:48:59 +02:00
16 changed files with 203 additions and 52 deletions
+9
View File
@@ -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
Binary file not shown.
+1
View File
@@ -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
+3
View File
@@ -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
+6 -6
View File
@@ -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
+8
View File
@@ -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)
+7 -2
View File
@@ -3,6 +3,8 @@
#pragma once
#include <SDL3/SDL.h>
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
+37 -5
View File
@@ -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<float>(i) * INTERVAL_V;
lines_.push_back(verticals[i]);
}
for (int i = 0; i < NUM_H; i++) {
horizontals[i].spawn_time_s = static_cast<float>(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<int>(START_Y),
static_cast<int>(END_X),
static_cast<int>(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<int>(START_Y + (DY * HEAD_T)),
static_cast<int>(END_X),
static_cast<int>(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<int>(prev_y),
static_cast<int>(NX),
static_cast<int>(NY),
line.brightness);
line.brightness,
0.0F,
Defaults::Playfield::GRID_COLOR);
prev_x = NX;
prev_y = NY;
}
+3 -8
View File
@@ -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<Uint8>(255.0F * (1.0F - T)),
.g = 255,
.b = 255,
.a = 255}};
.color = Defaults::Title::Colors::STARFIELD};
}
};
+1 -1
View File
@@ -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);
}
+28 -21
View File
@@ -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);
}
// ========================================
+55
View File
@@ -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<float>(Defaults::Window::WIDTH) / 2.0F,
.y = static_cast<float>(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);
}
}
+14
View File
@@ -63,6 +63,20 @@ class TitleScene final : public Scene {
std::unique_ptr<Graphics::Camera3D> camera_;
std::unique_ptr<Graphics::Starfield> starfield_;
std::unique_ptr<Title::ShipAnimator> 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<Flash, 2> flashes_{};
std::shared_ptr<Graphics::Shape> 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};
+18 -8
View File
@@ -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);
+6 -1
View File
@@ -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;
}
}
}
}
+7
View File
@@ -10,6 +10,7 @@
#include <array>
#include <cstdint>
#include <functional>
#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(int player_id)>;
void setOnShipDisappear(ShipDisappearCallback cb) { on_ship_disappear_ = std::move(cb); }
private:
Rendering::Renderer* renderer_;
const Graphics::Camera3D* camera_;
std::array<TitleShip, 2> ships_;
ShipDisappearCallback on_ship_disappear_;
static void updateEntering(TitleShip& ship, float delta_time);
static void updateFloating(TitleShip& ship, float delta_time);