Merge branch 'tweak/misc-adjustments': retocs varis (paleta, glow, audio, física, destell del títol)
This commit is contained in:
@@ -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.
@@ -39,6 +39,7 @@ namespace Defaults::Sound {
|
|||||||
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
|
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* 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* 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* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
|
||||||
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
|
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
|
||||||
constexpr const char* LOGO = "effects/logo.wav"; // Logo
|
constexpr const char* LOGO = "effects/logo.wav"; // Logo
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ namespace Defaults::Game {
|
|||||||
// (1.05F) per tolerar floating-point i petites separacions post-impuls.
|
// (1.05F) per tolerar floating-point i petites separacions post-impuls.
|
||||||
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 1.05F;
|
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 1.05F;
|
||||||
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
|
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
|
// Friendly fire system
|
||||||
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ namespace Defaults::Palette {
|
|||||||
// brillantor perceptual sota el bloom (sense alterar la identitat de color).
|
// 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ó
|
// El canal dominant es manté a 255 a cada color per maximitzar la saturació
|
||||||
// visible quan el halo s'expandeix.
|
// visible quan el halo s'expandeix.
|
||||||
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
|
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 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 PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador"
|
||||||
constexpr SDL_Color QUADRAT = {.r = 255, .g = 140, .b = 140, .a = 255}; // Rojo "tank"
|
constexpr SDL_Color QUADRAT = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank"
|
||||||
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 160, .b = 255, .a = 255}; // Magenta agresivo
|
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
|
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
|
||||||
|
|
||||||
} // namespace Defaults::Palette
|
} // namespace Defaults::Palette
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ namespace Defaults::Physics {
|
|||||||
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
|
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
|
||||||
} // namespace Bullet
|
} // namespace Bullet
|
||||||
|
|
||||||
|
// Ship → enemy: impuls explícit aplicat a l'enemic en el moment exacte
|
||||||
|
// que la nau mor per col·lisió amb ell (afegit per damunt del rebot
|
||||||
|
// natural de PhysicsWorld, que ja és present però subtil amb la
|
||||||
|
// damping de la nau).
|
||||||
|
namespace Ship {
|
||||||
|
constexpr float DEATH_IMPACT_MOMENTUM_FACTOR = 0.3F;
|
||||||
|
} // namespace Ship
|
||||||
|
|
||||||
// Explosions (debris physics)
|
// Explosions (debris physics)
|
||||||
namespace Debris {
|
namespace Debris {
|
||||||
constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s)
|
constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
namespace Defaults::Playfield {
|
namespace Defaults::Playfield {
|
||||||
|
|
||||||
// Estructura de la graella (cel·les omplen tota la PLAYAREA)
|
// 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
|
constexpr int SUBDIVISIONS = 4; // cada cel·la principal es divideix en N subcel·les
|
||||||
|
|
||||||
// Brillo respecte al color global (border = 1.0)
|
// Brillo respecte al color global (border = 1.0)
|
||||||
constexpr float GRID_BRIGHTNESS = 0.15F;
|
constexpr float GRID_BRIGHTNESS = 0.20F;
|
||||||
constexpr float SUBGRID_BRIGHTNESS = 0.05F;
|
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.
|
// Animació de creació amb timer intern del Playfield.
|
||||||
// L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en
|
// L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en
|
||||||
|
|||||||
@@ -196,13 +196,39 @@ namespace Graphics {
|
|||||||
lines_.clear();
|
lines_.clear();
|
||||||
lines_.reserve(verticals.size() + horizontals.size());
|
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++) {
|
for (int i = 0; i < NUM_V; i++) {
|
||||||
verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V;
|
verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V;
|
||||||
lines_.push_back(verticals[i]);
|
|
||||||
}
|
}
|
||||||
for (int i = 0; i < NUM_H; i++) {
|
for (int i = 0; i < NUM_H; i++) {
|
||||||
horizontals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_H;
|
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>(START_Y),
|
||||||
static_cast<int>(END_X),
|
static_cast<int>(END_X),
|
||||||
static_cast<int>(END_Y),
|
static_cast<int>(END_Y),
|
||||||
line.brightness);
|
line.brightness,
|
||||||
|
0.0F,
|
||||||
|
Defaults::Playfield::GRID_COLOR);
|
||||||
// Cap brillant mentre creix.
|
// Cap brillant mentre creix.
|
||||||
if (P < 1.0F) {
|
if (P < 1.0F) {
|
||||||
const float LENGTH = std::sqrt((DX * DX) + (DY * DY));
|
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>(START_Y + (DY * HEAD_T)),
|
||||||
static_cast<int>(END_X),
|
static_cast<int>(END_X),
|
||||||
static_cast<int>(END_Y),
|
static_cast<int>(END_Y),
|
||||||
Defaults::Playfield::HEAD_BRIGHTNESS);
|
Defaults::Playfield::HEAD_BRIGHTNESS,
|
||||||
|
0.0F,
|
||||||
|
Defaults::Playfield::GRID_COLOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -303,7 +333,9 @@ namespace Graphics {
|
|||||||
static_cast<int>(prev_y),
|
static_cast<int>(prev_y),
|
||||||
static_cast<int>(NX),
|
static_cast<int>(NX),
|
||||||
static_cast<int>(NY),
|
static_cast<int>(NY),
|
||||||
line.brightness);
|
line.brightness,
|
||||||
|
0.0F,
|
||||||
|
Defaults::Playfield::GRID_COLOR);
|
||||||
prev_x = NX;
|
prev_x = NX;
|
||||||
prev_y = NY;
|
prev_y = NY;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,20 +31,15 @@ namespace Graphics {
|
|||||||
const float MIN_Y = zona.y;
|
const float MIN_Y = zona.y;
|
||||||
const float MAX_Y = zona.y + zona.h;
|
const float MAX_Y = zona.y + zona.h;
|
||||||
|
|
||||||
// Tint aleatori entre blanc (255,255,255) i cyan (0,255,255) per estrella.
|
// Color únic per a totes les estrelles: el mateix blanc-blau gel
|
||||||
// T ∈ [0,1]: 0 → blanc; 1 → cyan. R = 255·(1-T), G=B=255.
|
// del starfield del títol (Defaults::Title::Colors::STARFIELD).
|
||||||
const auto FILL_LAYER = [&](int layer, int count, int& idx) {
|
const auto FILL_LAYER = [&](int layer, int count, int& idx) {
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
const float T = randUniform(0.0F, 1.0F);
|
|
||||||
stars_[idx++] = Star{
|
stars_[idx++] = Star{
|
||||||
.x = randUniform(MIN_X, MAX_X),
|
.x = randUniform(MIN_X, MAX_X),
|
||||||
.y = randUniform(MIN_Y, MAX_Y),
|
.y = randUniform(MIN_Y, MAX_Y),
|
||||||
.layer = layer,
|
.layer = layer,
|
||||||
.color = SDL_Color{
|
.color = Defaults::Title::Colors::STARFIELD};
|
||||||
.r = static_cast<Uint8>(255.0F * (1.0F - T)),
|
|
||||||
.g = 255,
|
|
||||||
.b = 255,
|
|
||||||
.a = 255}};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -182,5 +182,5 @@ void Ship::draw() const {
|
|||||||
|
|
||||||
void Ship::herir() {
|
void Ship::herir() {
|
||||||
hurt_timer_ = Defaults::Ship::Hurt::DURATION;
|
hurt_timer_ = Defaults::Ship::Hurt::DURATION;
|
||||||
Audio::get()->playSound(Defaults::Sound::HIT, Audio::Group::GAME);
|
Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -723,30 +723,36 @@ void GameScene::tocado(uint8_t player_id) {
|
|||||||
if (hit_timer_per_player_[player_id] == 0.0F) {
|
if (hit_timer_per_player_[player_id] == 0.0F) {
|
||||||
// *** PHASE 1: TRIGGER DEATH ***
|
// *** 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)
|
// Mark ship as dead (stops rendering and input)
|
||||||
ships_[player_id].markHit();
|
ships_[player_id].markHit();
|
||||||
|
|
||||||
// Create ship explosion
|
const Vec2 INHERITED_VEL = SHIP_VEL_PRE_DEATH *
|
||||||
const Vec2& ship_pos = ships_[player_id].getCenter();
|
Defaults::Physics::Debris::SHIP_VELOCITY_INHERITANCE;
|
||||||
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};
|
|
||||||
|
|
||||||
|
// Mateixa dispersió i efecte que els debris d'enemic (lifetime,
|
||||||
|
// friction, segment_multiplier alineats); només canvien sound i color.
|
||||||
debris_manager_.explode(
|
debris_manager_.explode(
|
||||||
ships_[player_id].getShape(), // Ship shape (3 lines)
|
ships_[player_id].getShape(),
|
||||||
ship_pos, // Center position
|
SHIP_POS,
|
||||||
ship_angle, // Ship orientation
|
SHIP_ANGLE,
|
||||||
1.0F, // Normal scale
|
1.0F,
|
||||||
Defaults::Physics::Debris::VELOCITAT_BASE, // 80 px/s
|
Defaults::Physics::Debris::VELOCITAT_BASE,
|
||||||
ships_[player_id].getBrightness(), // Heredar brightness
|
SHIP_BRIGHT,
|
||||||
vel_nau_80, // Heredar 80% velocity
|
INHERITED_VEL,
|
||||||
0.0F, // Nave: trayectorias rectas (sin drotacio)
|
0.0F, // sense herència angular
|
||||||
0.0F, // Sin herencia visual (rotación aleatoria)
|
0.0F, // sin herencia visual
|
||||||
Defaults::Sound::EXPLOSION2, // Sonido alternativo para la explosión
|
Defaults::Sound::EXPLOSION2,
|
||||||
Defaults::Palette::SHIP // Debris hereda color de la nave
|
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)
|
// Start death timer (non-zero to avoid re-triggering)
|
||||||
hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
|
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 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);
|
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};
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
@@ -73,6 +73,15 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
|||||||
// correcte de la intro coreografiada (també quan venim de JUMP_TO_TITLE_MAIN).
|
// correcte de la intro coreografiada (també quan venim de JUMP_TO_TITLE_MAIN).
|
||||||
ship_animator_->setVisible(false);
|
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();
|
initTitle();
|
||||||
inicialitzarJailgames();
|
inicialitzarJailgames();
|
||||||
|
|
||||||
@@ -294,6 +303,7 @@ void TitleScene::update(float delta_time) {
|
|||||||
estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) {
|
estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) {
|
||||||
ship_animator_->update(delta_time);
|
ship_animator_->update(delta_time);
|
||||||
}
|
}
|
||||||
|
updateFlashes(delta_time);
|
||||||
|
|
||||||
switch (estat_actual_) {
|
switch (estat_actual_) {
|
||||||
case TitleState::STARFIELD_FADE_IN:
|
case TitleState::STARFIELD_FADE_IN:
|
||||||
@@ -524,6 +534,7 @@ void TitleScene::draw() {
|
|||||||
estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) {
|
estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) {
|
||||||
ship_animator_->draw();
|
ship_animator_->draw();
|
||||||
}
|
}
|
||||||
|
drawFlashes();
|
||||||
|
|
||||||
if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) {
|
if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) {
|
||||||
return;
|
return;
|
||||||
@@ -629,3 +640,47 @@ auto TitleScene::checkStartGameButtonPressed() -> bool {
|
|||||||
void TitleScene::handleEvent(const SDL_Event& event) {
|
void TitleScene::handleEvent(const SDL_Event& event) {
|
||||||
(void)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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,20 @@ class TitleScene final : public Scene {
|
|||||||
std::unique_ptr<Graphics::Camera3D> camera_;
|
std::unique_ptr<Graphics::Camera3D> camera_;
|
||||||
std::unique_ptr<Graphics::Starfield> starfield_;
|
std::unique_ptr<Graphics::Starfield> starfield_;
|
||||||
std::unique_ptr<Title::ShipAnimator> ship_animator_;
|
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};
|
TitleState estat_actual_{TitleState::STARFIELD_FADE_IN};
|
||||||
float temps_acumulat_{0.0F};
|
float temps_acumulat_{0.0F};
|
||||||
|
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ namespace Systems::Collision {
|
|||||||
if (A_WOUNDED == B_WOUNDED) {
|
if (A_WOUNDED == B_WOUNDED) {
|
||||||
continue; // ambos sanos o ambos heridos: nada que propagar
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
// El sano queda herido, propagando el shooter original.
|
// 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.
|
// Comprovem si la nau toca QUALSEVOL enemic vulnerable aquest frame.
|
||||||
bool touching_now = false;
|
Enemy* touched_enemy = nullptr;
|
||||||
for (const auto& enemy : ctx.enemies) {
|
for (auto& enemy : ctx.enemies) {
|
||||||
if (enemy.isInvulnerable()) {
|
if (enemy.isInvulnerable()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) {
|
if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) {
|
||||||
touching_now = true;
|
touched_enemy = &enemy;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const bool TOUCHING_NOW = touched_enemy != nullptr;
|
||||||
|
|
||||||
// Edge-trigger: només compta com a impacte la transició no-tocant → tocant.
|
// 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
|
// Així el contacte continu durant el rebot frame-a-frame no dispara HURT i mort
|
||||||
// en frames consecutius.
|
// 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 (RISING_EDGE) {
|
||||||
if (ctx.ships[i].isHurt()) {
|
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);
|
ctx.on_player_hit(i);
|
||||||
} else {
|
} else {
|
||||||
// Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld;
|
// 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].herir();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.ships[i].setTouchingEnemyPrevFrame(touching_now);
|
ctx.ships[i].setTouchingEnemyPrevFrame(TOUCHING_NOW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +273,12 @@ namespace Systems::Collision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// *** TEAMMATE HIT (friendly fire) ***
|
// *** 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.on_player_hit(player_id);
|
||||||
ctx.lives_per_player[BULLET_OWNER]++;
|
ctx.lives_per_player[BULLET_OWNER]++;
|
||||||
Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME);
|
Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME);
|
||||||
|
|||||||
@@ -142,9 +142,14 @@ namespace Title {
|
|||||||
case ShipState::FLOATING:
|
case ShipState::FLOATING:
|
||||||
updateFloating(ship, delta_time);
|
updateFloating(ship, delta_time);
|
||||||
break;
|
break;
|
||||||
case ShipState::EXITING:
|
case ShipState::EXITING: {
|
||||||
updateExiting(ship, delta_time);
|
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;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include "core/graphics/camera3d.hpp"
|
#include "core/graphics/camera3d.hpp"
|
||||||
#include "core/graphics/wireframe3d.hpp"
|
#include "core/graphics/wireframe3d.hpp"
|
||||||
@@ -70,10 +71,16 @@ namespace Title {
|
|||||||
[[nodiscard]] auto isAnimationComplete() const -> bool;
|
[[nodiscard]] auto isAnimationComplete() const -> bool;
|
||||||
[[nodiscard]] auto isVisible() 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:
|
private:
|
||||||
Rendering::Renderer* renderer_;
|
Rendering::Renderer* renderer_;
|
||||||
const Graphics::Camera3D* camera_;
|
const Graphics::Camera3D* camera_;
|
||||||
std::array<TitleShip, 2> ships_;
|
std::array<TitleShip, 2> ships_;
|
||||||
|
ShipDisappearCallback on_ship_disappear_;
|
||||||
|
|
||||||
static void updateEntering(TitleShip& ship, float delta_time);
|
static void updateEntering(TitleShip& ship, float delta_time);
|
||||||
static void updateFloating(TitleShip& ship, float delta_time);
|
static void updateFloating(TitleShip& ship, float delta_time);
|
||||||
|
|||||||
Reference in New Issue
Block a user