Files
orni_attack/source/game/escenes/escena_joc.cpp
2025-12-10 17:18:34 +01:00

1022 lines
37 KiB
C++

// escena_joc.cpp - Implementació de la lògica del joc
// © 1999 Visente i Sergi (versió Pascal)
// © 2025 Port a C++20 amb SDL3
#include "escena_joc.hpp"
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <vector>
#include "core/audio/audio.hpp"
#include "core/input/mouse.hpp"
#include "core/math/easing.hpp"
#include "core/rendering/line_renderer.hpp"
#include "core/system/context_escenes.hpp"
#include "core/system/global_events.hpp"
#include "game/stage_system/stage_loader.hpp"
// Using declarations per simplificar el codi
using GestorEscenes::ContextEscenes;
using Escena = ContextEscenes::Escena;
using Opcio = ContextEscenes::Opcio;
EscenaJoc::EscenaJoc(SDLManager& sdl, ContextEscenes& context)
: sdl_(sdl),
context_(context),
debris_manager_(sdl.obte_renderer()),
gestor_puntuacio_(sdl.obte_renderer()),
text_(sdl.obte_renderer()) {
// Consumir opcions (preparació per MODE_DEMO futur)
auto opcio = context_.consumir_opcio();
(void)opcio; // Suprimir warning de variable no usada
// Inicialitzar naus amb renderer (P1=ship.shp, P2=ship2.shp)
naus_[0] = Nau(sdl.obte_renderer(), "ship.shp"); // Jugador 1: nave estàndar
naus_[1] = Nau(sdl.obte_renderer(), "ship2.shp"); // Jugador 2: interceptor amb ales
// Inicialitzar bales amb renderer
for (auto& bala : bales_) {
bala = Bala(sdl.obte_renderer());
}
// Inicialitzar enemics amb renderer
for (auto& enemy : orni_) {
enemy = Enemic(sdl.obte_renderer());
}
}
void EscenaJoc::executar() {
std::cout << "Escena Joc: Inicialitzant...\n";
// Inicialitzar estat del joc
inicialitzar();
SDL_Event event;
Uint64 last_time = SDL_GetTicks();
while (GestorEscenes::actual == Escena::JOC) {
// Calcular delta_time real
Uint64 current_time = SDL_GetTicks();
float delta_time = (current_time - last_time) / 1000.0f;
last_time = current_time;
// Limitar delta_time per evitar grans salts
if (delta_time > 0.05f) {
delta_time = 0.05f;
}
// Actualitzar comptador de FPS
sdl_.updateFPS(delta_time);
// Actualitzar visibilitat del cursor (auto-ocultar)
Mouse::updateCursorVisibility();
// Processar events SDL
while (SDL_PollEvent(&event)) {
// Manejo de finestra
if (sdl_.handleWindowEvent(event)) {
continue;
}
// Events globals (F1/F2/F3/ESC/QUIT)
if (GlobalEvents::handle(event, sdl_, context_)) {
continue;
}
// Processament específic del joc (SPACE per disparar)
processar_input(event);
}
// Actualitzar física del joc amb delta_time real
actualitzar(delta_time);
// Actualitzar sistema d'audio
Audio::update();
// Actualitzar colors oscil·lats
sdl_.updateColors(delta_time);
// Netejar pantalla (usa color oscil·lat)
sdl_.neteja(0, 0, 0);
// Actualitzar context de renderitzat (factor d'escala global)
sdl_.updateRenderingContext();
// Dibuixar joc
dibuixar();
// Presentar renderer (swap buffers)
sdl_.presenta();
}
std::cout << "Escena Joc: Finalitzant...\n";
}
void EscenaJoc::inicialitzar() {
// Inicialitzar generador de números aleatoris
// Basat en el codi Pascal original: line 376
std::srand(static_cast<unsigned>(std::time(nullptr)));
// [NEW] Load stage configuration (only once)
if (!stage_config_) {
stage_config_ = StageSystem::StageLoader::carregar("data/stages/stages.yaml");
if (!stage_config_) {
std::cerr << "[EscenaJoc] Error: no s'ha pogut carregar stages.yaml" << std::endl;
// Continue without stage system (will crash, but helps debugging)
}
}
// [NEW] Initialize stage manager
stage_manager_ = std::make_unique<StageSystem::StageManager>(stage_config_.get());
stage_manager_->inicialitzar();
// [NEW] Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
stage_manager_->get_spawn_controller().set_ship_position(&naus_[0].get_centre());
// Inicialitzar timers de muerte per jugador
itocado_per_jugador_[0] = 0.0f;
itocado_per_jugador_[1] = 0.0f;
// Initialize lives and game over state (independent lives per player)
vides_per_jugador_[0] = Defaults::Game::STARTING_LIVES;
vides_per_jugador_[1] = Defaults::Game::STARTING_LIVES;
game_over_ = false;
game_over_timer_ = 0.0f;
// Initialize scores (separate per player)
puntuacio_per_jugador_[0] = 0;
puntuacio_per_jugador_[1] = 0;
gestor_puntuacio_.reiniciar();
// Set spawn point to center X, 75% Y (legacy, for INIT_HUD animation)
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
punt_spawn_.x = zona.x + zona.w * 0.5f;
punt_spawn_.y = zona.y + zona.h * Defaults::Game::INIT_HUD_SHIP_START_Y_RATIO;
// Inicialitzar AMBAS naus amb posicions específiques
for (uint8_t i = 0; i < 2; i++) {
Punt spawn_pos = obtenir_punt_spawn(i);
naus_[i].inicialitzar(&spawn_pos, false); // No invulnerability at start
}
// [MODIFIED] Initialize enemies as inactive (stage system will spawn them)
for (auto& enemy : orni_) {
enemy = Enemic(sdl_.obte_renderer());
enemy.set_ship_position(&naus_[0].get_centre()); // Set ship reference (P1 for now)
// DON'T call enemy.inicialitzar() here - stage system handles spawning
}
// Inicialitzar bales (now 6 instead of 3)
for (auto& bala : bales_) {
bala.inicialitzar();
}
// [ELIMINAT] Iniciar música de joc (ara es gestiona en stage_manager)
// La música s'inicia quan es transiciona de INIT_HUD a LEVEL_START
// Audio::get()->playMusic("game.ogg");
}
void EscenaJoc::actualitzar(float delta_time) {
// Check game over state first
if (game_over_) {
// Game over: only update timer, enemies, bullets, and debris
game_over_timer_ -= delta_time;
if (game_over_timer_ <= 0.0f) {
// Aturar música de joc abans de tornar al títol
Audio::get()->stopMusic();
// Transició a pantalla de títol
context_.canviar_escena(Escena::TITOL);
GestorEscenes::actual = Escena::TITOL;
return;
}
// Enemies and bullets continue moving during game over
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
}
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.actualitzar(delta_time);
return;
}
// Check death sequence state for BOTH players
bool algun_jugador_mort = false;
for (uint8_t i = 0; i < 2; i++) {
if (itocado_per_jugador_[i] > 0.0f && itocado_per_jugador_[i] < 999.0f) {
algun_jugador_mort = true;
// Death sequence active: update timer
itocado_per_jugador_[i] += delta_time;
// Check if death duration completed (only trigger ONCE using sentinel value)
if (itocado_per_jugador_[i] >= Defaults::Game::DEATH_DURATION) {
// *** PHASE 3: RESPAWN OR GAME OVER ***
// Decrement lives for this player (only once)
vides_per_jugador_[i]--;
if (vides_per_jugador_[i] > 0) {
// Respawn ship en spawn position con invulnerabilidad
Punt spawn_pos = obtenir_punt_spawn(i);
naus_[i].inicialitzar(&spawn_pos, true);
itocado_per_jugador_[i] = 0.0f;
} else {
// Player is permanently dead (out of lives)
// Set sentinel value to prevent re-entering this block
itocado_per_jugador_[i] = 999.0f;
// Check if BOTH players are dead (game over)
if (vides_per_jugador_[0] <= 0 && vides_per_jugador_[1] <= 0) {
game_over_ = true;
game_over_timer_ = Defaults::Game::GAME_OVER_DURATION;
}
}
}
}
}
// If any player is dead, still update enemies/bullets/effects
if (algun_jugador_mort) {
// Enemies and bullets continue moving during death sequence
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
}
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.actualitzar(delta_time);
// Don't return - allow alive players to continue playing
}
// *** STAGE SYSTEM STATE MACHINE ***
StageSystem::EstatStage estat = stage_manager_->get_estat();
switch (estat) {
case StageSystem::EstatStage::INIT_HUD: {
// Update stage manager timer (pot canviar l'estat!)
stage_manager_->actualitzar(delta_time);
// [FIX] Si l'estat ha canviat durant actualitzar(), sortir immediatament
// per evitar recalcular la posició de la nau amb el nou timer
if (stage_manager_->get_estat() != StageSystem::EstatStage::INIT_HUD) {
break;
}
// Calcular progrés de l'animació de la nau
float ship_progress = 1.0f - (stage_manager_->get_timer_transicio() /
Defaults::Game::INIT_HUD_DURATION);
ship_progress = std::min(1.0f, ship_progress);
// Calcular quant ha avançat l'animació de la nau
float ship_anim_progress = ship_progress / Defaults::Game::INIT_HUD_SHIP_RATIO;
ship_anim_progress = std::min(1.0f, ship_anim_progress);
// Actualitzar posició de la nau P1 segons animació (P2 apareix directamente)
if (ship_anim_progress < 1.0f) {
Punt pos_animada = calcular_posicio_nau_init_hud(ship_anim_progress);
naus_[0].set_centre(pos_animada); // Solo P1 animación
}
// Una vegada l'animació acaba, permetre control normal
// però mantenir la posició inicial especial fins LEVEL_START
break;
}
case StageSystem::EstatStage::LEVEL_START: {
// [DEBUG] Log entrada a LEVEL_START
static bool first_entry = true;
if (first_entry) {
std::cout << "[LEVEL_START] ENTERED with P1 pos.y=" << naus_[0].get_centre().y << std::endl;
first_entry = false;
}
// Update countdown timer
stage_manager_->actualitzar(delta_time);
// [NEW] Allow both ships movement and shooting during intro
for (uint8_t i = 0; i < 2; i++) {
if (itocado_per_jugador_[i] == 0.0f) { // Only alive players
naus_[i].processar_input(delta_time, i);
naus_[i].actualitzar(delta_time);
}
}
// [NEW] Update bullets
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
// [NEW] Update debris
debris_manager_.actualitzar(delta_time);
break;
}
case StageSystem::EstatStage::PLAYING: {
// [NEW] Update stage manager (spawns enemies, pause if BOTH dead)
bool pausar_spawn = (itocado_per_jugador_[0] > 0.0f && itocado_per_jugador_[1] > 0.0f);
stage_manager_->get_spawn_controller().actualitzar(delta_time, orni_, pausar_spawn);
// [NEW] Check stage completion (only when at least one player alive)
bool algun_jugador_viu = (itocado_per_jugador_[0] == 0.0f || itocado_per_jugador_[1] == 0.0f);
if (algun_jugador_viu) {
auto& spawn_ctrl = stage_manager_->get_spawn_controller();
if (spawn_ctrl.tots_enemics_destruits(orni_)) {
stage_manager_->stage_completat();
Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
break;
}
}
// [EXISTING] Normal gameplay - update BOTH players
for (uint8_t i = 0; i < 2; i++) {
if (itocado_per_jugador_[i] == 0.0f) { // Only alive players
naus_[i].processar_input(delta_time, i);
naus_[i].actualitzar(delta_time);
}
}
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
}
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
detectar_col·lisions_bales_enemics();
detectar_col·lisio_naus_enemics();
debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.actualitzar(delta_time);
break;
}
case StageSystem::EstatStage::LEVEL_COMPLETED:
// Update countdown timer
stage_manager_->actualitzar(delta_time);
// [NEW] Allow both ships movement and shooting during outro
for (uint8_t i = 0; i < 2; i++) {
if (itocado_per_jugador_[i] == 0.0f) { // Only alive players
naus_[i].processar_input(delta_time, i);
naus_[i].actualitzar(delta_time);
}
}
// [NEW] Update bullets (allow last shots to continue)
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
// [NEW] Update debris (from last destroyed enemies)
debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.actualitzar(delta_time);
break;
}
}
void EscenaJoc::dibuixar() {
// Check game over state
if (game_over_) {
// Game over: draw enemies, bullets, debris, and "GAME OVER" text
dibuixar_marges();
for (const auto& enemy : orni_) {
enemy.dibuixar();
}
for (const auto& bala : bales_) {
bala.dibuixar();
}
debris_manager_.dibuixar();
gestor_puntuacio_.dibuixar();
// Draw centered "GAME OVER" text
const std::string game_over_text = "GAME OVER";
constexpr float escala = 2.0f;
constexpr float spacing = 4.0f;
float text_width = text_.get_text_width(game_over_text, escala, spacing);
float text_height = text_.get_text_height(escala);
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
float x = play_area.x + (play_area.w - text_width) / 2.0f;
float y = play_area.y + (play_area.h - text_height) / 2.0f;
text_.render(game_over_text, {x, y}, escala, spacing);
dibuixar_marcador();
return;
}
// [NEW] Stage state rendering
StageSystem::EstatStage estat = stage_manager_->get_estat();
switch (estat) {
case StageSystem::EstatStage::INIT_HUD: {
// Calcular progrés de cada animació independent
float timer = stage_manager_->get_timer_transicio();
float total_time = Defaults::Game::INIT_HUD_DURATION;
float global_progress = 1.0f - (timer / total_time);
// Progrés del rectangle (empieza inmediatament)
float rect_progress = global_progress / Defaults::Game::INIT_HUD_RECT_RATIO;
rect_progress = std::min(1.0f, rect_progress);
// Progrés del marcador (empieza inmediatament)
float score_progress = global_progress / Defaults::Game::INIT_HUD_SCORE_RATIO;
score_progress = std::min(1.0f, score_progress);
// Progrés de la nau (empieza inmediatament)
float ship_progress = global_progress / Defaults::Game::INIT_HUD_SHIP_RATIO;
ship_progress = std::min(1.0f, ship_progress);
// Dibuixar elements animats
if (rect_progress > 0.0f) {
dibuixar_marges_animat(rect_progress);
}
if (score_progress > 0.0f) {
dibuixar_marcador_animat(score_progress);
}
// Dibuixar nau P1 (usant el sistema normal, la posició ja està actualitzada)
// Durante INIT_HUD solo se anima P1
if (ship_progress > 0.0f && !naus_[0].esta_tocada()) {
naus_[0].dibuixar();
}
break;
}
case StageSystem::EstatStage::LEVEL_START:
dibuixar_marges();
// [NEW] Draw both ships if alive
for (uint8_t i = 0; i < 2; i++) {
if (itocado_per_jugador_[i] == 0.0f) {
naus_[i].dibuixar();
}
}
// [NEW] Draw bullets
for (const auto& bala : bales_) {
bala.dibuixar();
}
// [NEW] Draw debris
debris_manager_.dibuixar();
gestor_puntuacio_.dibuixar();
// [EXISTING] Draw intro message and score
dibuixar_missatge_stage(stage_manager_->get_missatge_level_start());
dibuixar_marcador();
break;
case StageSystem::EstatStage::PLAYING:
dibuixar_marges();
// [EXISTING] Normal rendering - both ships
for (uint8_t i = 0; i < 2; i++) {
if (itocado_per_jugador_[i] == 0.0f) {
naus_[i].dibuixar();
}
}
for (const auto& enemy : orni_) {
enemy.dibuixar();
}
for (const auto& bala : bales_) {
bala.dibuixar();
}
debris_manager_.dibuixar();
gestor_puntuacio_.dibuixar();
dibuixar_marcador();
break;
case StageSystem::EstatStage::LEVEL_COMPLETED:
dibuixar_marges();
// [NEW] Draw both ships if alive
for (uint8_t i = 0; i < 2; i++) {
if (itocado_per_jugador_[i] == 0.0f) {
naus_[i].dibuixar();
}
}
// [NEW] Draw bullets (allow last shots to be visible)
for (const auto& bala : bales_) {
bala.dibuixar();
}
// [NEW] Draw debris (from last destroyed enemies)
debris_manager_.dibuixar();
gestor_puntuacio_.dibuixar();
// [EXISTING] Draw completion message and score
dibuixar_missatge_stage(StageSystem::Constants::MISSATGE_LEVEL_COMPLETED);
dibuixar_marcador();
break;
}
}
void EscenaJoc::processar_input(const SDL_Event& event) {
// Ignore ship controls during game over
if (game_over_) {
return;
}
// Processament d'input per events puntuals (no continus)
// L'input continu (fletxes/WASD) es processa en actualitzar() amb
// SDL_GetKeyboardState()
if (event.type == SDL_EVENT_KEY_DOWN) {
// P1 shoot
if (event.key.key == Defaults::Controls::P1::SHOOT) {
disparar_bala(0);
}
// P2 shoot
else if (event.key.key == Defaults::Controls::P2::SHOOT) {
disparar_bala(1);
}
}
}
void EscenaJoc::tocado(uint8_t player_id) {
// Death sequence: 3 phases
// Phase 1: First call (itocado_per_jugador_[player_id] == 0) - trigger explosion
// Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation
// Phase 3: Respawn or game over (itocado_ >= 3.0s) - handled in actualitzar()
if (itocado_per_jugador_[player_id] == 0.0f) {
// *** PHASE 1: TRIGGER DEATH ***
// Mark ship as dead (stops rendering and input)
naus_[player_id].marcar_tocada();
// Create ship explosion
const Punt& ship_pos = naus_[player_id].get_centre();
float ship_angle = naus_[player_id].get_angle();
Punt vel_nau = naus_[player_id].get_velocitat_vector();
// Reduir a 80% la velocitat heretada per la nau (més realista)
Punt vel_nau_80 = {vel_nau.x * 0.8f, vel_nau.y * 0.8f};
debris_manager_.explotar(
naus_[player_id].get_forma(), // Ship shape (3 lines)
ship_pos, // Center position
ship_angle, // Ship orientation
1.0f, // Normal scale
Defaults::Physics::Debris::VELOCITAT_BASE, // 80 px/s
naus_[player_id].get_brightness(), // Heredar brightness
vel_nau_80, // Heredar 80% velocitat
0.0f, // Nave: trayectorias rectas (sin drotacio)
0.0f // Sin herencia visual (rotación aleatoria)
);
Audio::get()->playSound(Defaults::Sound::EXPLOSION, Audio::Group::GAME);
// Start death timer (non-zero to avoid re-triggering)
itocado_per_jugador_[player_id] = 0.001f;
}
// Phase 2 is automatic (debris updates in actualitzar())
// Phase 3 is handled in actualitzar() when itocado_per_jugador_ >= DEATH_DURATION
}
void EscenaJoc::dibuixar_marges() const {
// Dibuixar rectangle de la zona de joc
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
// Coordenades dels cantons
int x1 = static_cast<int>(zona.x);
int y1 = static_cast<int>(zona.y);
int x2 = static_cast<int>(zona.x + zona.w);
int y2 = static_cast<int>(zona.y + zona.h);
// 4 línies per formar el rectangle
Rendering::linea(sdl_.obte_renderer(), x1, y1, x2, y1, true); // Top
Rendering::linea(sdl_.obte_renderer(), x1, y2, x2, y2, true); // Bottom
Rendering::linea(sdl_.obte_renderer(), x1, y1, x1, y2, true); // Left
Rendering::linea(sdl_.obte_renderer(), x2, y1, x2, y2, true); // Right
}
void EscenaJoc::dibuixar_marcador() {
// Construir text del marcador
std::string text = construir_marcador();
// Paràmetres de renderització
const float escala = 0.85f;
const float spacing = 0.0f;
// Calcular dimensions del text
float text_width = text_.get_text_width(text, escala, spacing);
float text_height = text_.get_text_height(escala);
// Centrat horitzontal dins de la zona del marcador
float x = (Defaults::Zones::SCOREBOARD.w - text_width) / 2.0f;
// Centrat vertical dins de la zona del marcador
float y = Defaults::Zones::SCOREBOARD.y +
(Defaults::Zones::SCOREBOARD.h - text_height) / 2.0f;
// Renderitzar
text_.render(text, {x, y}, escala, spacing);
}
void EscenaJoc::dibuixar_marges_animat(float progress) const {
// Animació seqüencial del rectangle amb efecte de "pinzell"
// Dos pinzells comencen al centre superior i baixen pels laterals
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
// Aplicar easing al progrés global
float eased_progress = Easing::ease_out_quad(progress);
// Coordenades del rectangle complet
int x1 = static_cast<int>(zona.x);
int y1 = static_cast<int>(zona.y);
int x2 = static_cast<int>(zona.x + zona.w);
int y2 = static_cast<int>(zona.y + zona.h);
int cx = (x1 + x2) / 2;
// Dividir en 3 fases de 33% cada una
constexpr float PHASE_1_END = 0.33f;
constexpr float PHASE_2_END = 0.66f;
// --- FASE 1: Línies horitzontals superiors (0-33%) ---
if (eased_progress > 0.0f) {
float phase1_progress = std::min(eased_progress / PHASE_1_END, 1.0f);
// Línia esquerra: creix des del centre cap a l'esquerra
int x1_phase1 = static_cast<int>(cx - (cx - x1) * phase1_progress);
Rendering::linea(sdl_.obte_renderer(), cx, y1, x1_phase1, y1, true);
// Línia dreta: creix des del centre cap a la dreta
int x2_phase1 = static_cast<int>(cx + (x2 - cx) * phase1_progress);
Rendering::linea(sdl_.obte_renderer(), cx, y1, x2_phase1, y1, true);
}
// --- FASE 2: Línies verticals laterals (33-66%) ---
if (eased_progress > PHASE_1_END) {
float phase2_progress = std::min((eased_progress - PHASE_1_END) / (PHASE_2_END - PHASE_1_END), 1.0f);
// Línia esquerra: creix des de dalt cap a baix
int y2_phase2 = static_cast<int>(y1 + (y2 - y1) * phase2_progress);
Rendering::linea(sdl_.obte_renderer(), x1, y1, x1, y2_phase2, true);
// Línia dreta: creix des de dalt cap a baix
Rendering::linea(sdl_.obte_renderer(), x2, y1, x2, y2_phase2, true);
}
// --- FASE 3: Línies horitzontals inferiors (66-100%) ---
if (eased_progress > PHASE_2_END) {
float phase3_progress = (eased_progress - PHASE_2_END) / (1.0f - PHASE_2_END);
// Línia esquerra: creix des de l'esquerra cap al centre
int x_left_phase3 = static_cast<int>(x1 + (cx - x1) * phase3_progress);
Rendering::linea(sdl_.obte_renderer(), x1, y2, x_left_phase3, y2, true);
// Línia dreta: creix des de la dreta cap al centre
int x_right_phase3 = static_cast<int>(x2 - (x2 - cx) * phase3_progress);
Rendering::linea(sdl_.obte_renderer(), x2, y2, x_right_phase3, y2, true);
}
}
void EscenaJoc::dibuixar_marcador_animat(float progress) {
// Animació del marcador pujant des de baix amb easing
// Calcular progrés amb easing
float eased_progress = Easing::ease_out_quad(progress);
// Construir text
std::string text = construir_marcador();
// Paràmetres
const float escala = 0.85f;
const float spacing = 0.0f;
// Calcular dimensions
float text_width = text_.get_text_width(text, escala, spacing);
float text_height = text_.get_text_height(escala);
// Posició X final (centrada horitzontalment)
float x_final = (Defaults::Zones::SCOREBOARD.w - text_width) / 2.0f;
// Posició Y final (centrada verticalment en la zona de scoreboard)
float y_final = Defaults::Zones::SCOREBOARD.y +
(Defaults::Zones::SCOREBOARD.h - text_height) / 2.0f;
// Posició Y inicial (offscreen, sota de la pantalla)
float y_inicial = static_cast<float>(Defaults::Game::HEIGHT) + text_height;
// Interpolació amb easing
float y_animada = y_inicial + (y_final - y_inicial) * eased_progress;
// Renderitzar en posició animada
text_.render(text, {x_final, y_animada}, escala, spacing);
}
Punt EscenaJoc::calcular_posicio_nau_init_hud(float progress) const {
// Animació de la nau pujant des de baix amb easing
// Calcular progrés amb easing
float eased_progress = Easing::ease_out_quad(progress);
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
// Posició X final (centre de la zona de joc)
float x_final = zona.x + zona.w / 2.0f;
// Posició Y final (75% de l'altura de la zona de joc)
float y_final = zona.y + zona.h * Defaults::Game::INIT_HUD_SHIP_START_Y_RATIO;
// Posició Y inicial (offscreen, sota de la zona de joc)
float y_inicial = zona.y + zona.h + 50.0f; // 50px sota
// X no canvia (sempre centrada)
// Y interpola amb easing
float y_animada = y_inicial + (y_final - y_inicial) * eased_progress;
return {x_final, y_animada};
}
std::string EscenaJoc::construir_marcador() const {
// Puntuació P1 (6 dígits)
std::string score_p1 = std::to_string(puntuacio_per_jugador_[0]);
score_p1 = std::string(6 - std::min(6, static_cast<int>(score_p1.length())), '0') + score_p1;
// Vides P1 (2 dígits)
std::string vides_p1 = (vides_per_jugador_[0] < 10)
? "0" + std::to_string(vides_per_jugador_[0])
: std::to_string(vides_per_jugador_[0]);
// Nivell (2 dígits)
uint8_t stage_num = stage_manager_->get_stage_actual();
std::string stage_str = (stage_num < 10) ? "0" + std::to_string(stage_num)
: std::to_string(stage_num);
// Puntuació P2 (6 dígits)
std::string score_p2 = std::to_string(puntuacio_per_jugador_[1]);
score_p2 = std::string(6 - std::min(6, static_cast<int>(score_p2.length())), '0') + score_p2;
// Vides P2 (2 dígits)
std::string vides_p2 = (vides_per_jugador_[1] < 10)
? "0" + std::to_string(vides_per_jugador_[1])
: std::to_string(vides_per_jugador_[1]);
// Format: "123456 03 LEVEL 01 654321 02"
// Nota: dos espais entre seccions
return score_p1 + " " + vides_p1 + " LEVEL " + stage_str + " " + score_p2 + " " + vides_p2;
}
void EscenaJoc::detectar_col·lisions_bales_enemics() {
// Constants amplificades per hitbox més generós (115%)
constexpr float RADI_BALA = Defaults::Entities::BULLET_RADIUS;
constexpr float RADI_ENEMIC = Defaults::Entities::ENEMY_RADIUS;
constexpr float SUMA_RADIS = (RADI_BALA + RADI_ENEMIC) * 1.15f; // 28.75 px
constexpr float SUMA_RADIS_QUADRAT = SUMA_RADIS * SUMA_RADIS; // 826.56
// Velocitat d'explosió reduïda per efecte suau
constexpr float VELOCITAT_EXPLOSIO = 80.0f; // px/s (en lloc de 80.0f per defecte)
// Iterar per totes les bales actives
for (auto& bala : bales_) {
if (!bala.esta_activa()) {
continue;
}
const Punt& pos_bala = bala.get_centre();
// Comprovar col·lisió amb tots els enemics actius
for (auto& enemic : orni_) {
if (!enemic.esta_actiu()) {
continue;
}
const Punt& pos_enemic = enemic.get_centre();
// Calcular distància quadrada (evita sqrt)
float dx = pos_bala.x - pos_enemic.x;
float dy = pos_bala.y - pos_enemic.y;
float distancia_quadrada = dx * dx + dy * dy;
// Comprovar col·lisió
if (distancia_quadrada <= SUMA_RADIS_QUADRAT) {
// *** COL·LISIÓ DETECTADA ***
// 1. Calculate score for enemy type
int punts = 0;
switch (enemic.get_tipus()) {
case TipusEnemic::PENTAGON:
punts = Defaults::Enemies::Scoring::PENTAGON_SCORE;
break;
case TipusEnemic::QUADRAT:
punts = Defaults::Enemies::Scoring::QUADRAT_SCORE;
break;
case TipusEnemic::MOLINILLO:
punts = Defaults::Enemies::Scoring::MOLINILLO_SCORE;
break;
}
// 2. Add score to the player who shot it
uint8_t owner_id = bala.get_owner_id();
puntuacio_per_jugador_[owner_id] += punts;
// 3. Create floating score number
gestor_puntuacio_.crear(punts, pos_enemic);
// 4. Destruir enemic (marca com inactiu)
enemic.destruir();
// 2. Crear explosió de fragments
Punt vel_enemic = enemic.get_velocitat_vector();
debris_manager_.explotar(
enemic.get_forma(), // Forma vectorial del pentàgon
pos_enemic, // Posició central
0.0f, // Angle (enemic té rotació interna)
1.0f, // Escala normal
VELOCITAT_EXPLOSIO, // 50 px/s (explosió suau)
enemic.get_brightness(), // Heredar brightness
vel_enemic, // Heredar velocitat
enemic.get_drotacio(), // Heredar velocitat angular (trayectorias curvas)
0.0f // Sin herencia visual (rotación aleatoria)
);
// 3. Desactivar bala
bala.desactivar();
// 4. Eixir del bucle intern (bala només destrueix 1 enemic)
break;
}
}
}
}
void EscenaJoc::detectar_col·lisio_naus_enemics() {
// Generous collision detection (80% hitbox)
constexpr float RADI_NAU = Defaults::Entities::SHIP_RADIUS;
constexpr float RADI_ENEMIC = Defaults::Entities::ENEMY_RADIUS;
constexpr float SUMA_RADIS =
(RADI_NAU + RADI_ENEMIC) * Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
constexpr float SUMA_RADIS_QUADRAT = SUMA_RADIS * SUMA_RADIS;
// Check collision for BOTH players
for (uint8_t i = 0; i < 2; i++) {
// Skip collisions if player is dead or invulnerable
if (itocado_per_jugador_[i] > 0.0f) continue;
if (!naus_[i].esta_viva()) continue;
if (naus_[i].es_invulnerable()) continue;
const Punt& pos_nau = naus_[i].get_centre();
// Check collision with all active enemies
for (const auto& enemic : orni_) {
if (!enemic.esta_actiu()) {
continue;
}
// Skip collision if enemy is invulnerable
if (enemic.es_invulnerable()) {
continue;
}
const Punt& pos_enemic = enemic.get_centre();
// Calculate squared distance (avoid sqrt)
float dx = static_cast<float>(pos_nau.x - pos_enemic.x);
float dy = static_cast<float>(pos_nau.y - pos_enemic.y);
float distancia_quadrada = dx * dx + dy * dy;
// Check collision
if (distancia_quadrada <= SUMA_RADIS_QUADRAT) {
tocado(i); // Trigger death sequence for player i
break; // Only one collision per player per frame
}
}
}
}
// [NEW] Stage system helper methods
void EscenaJoc::dibuixar_missatge_stage(const std::string& missatge) {
constexpr float escala_base = 1.0f;
constexpr float spacing = 2.0f;
constexpr float max_width_ratio = 0.9f; // 90% del ancho disponible
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
const float max_width = play_area.w * max_width_ratio; // 558px
// ========== TYPEWRITER EFFECT (PARAMETRIZED) ==========
// Get state-specific timing configuration
float total_time;
float typing_ratio;
if (stage_manager_->get_estat() == StageSystem::EstatStage::LEVEL_START) {
total_time = Defaults::Game::LEVEL_START_DURATION;
typing_ratio = Defaults::Game::LEVEL_START_TYPING_RATIO;
} else { // LEVEL_COMPLETED
total_time = Defaults::Game::LEVEL_COMPLETED_DURATION;
typing_ratio = Defaults::Game::LEVEL_COMPLETED_TYPING_RATIO;
}
// Calculate progress from timer (0.0 at start → 1.0 at end)
float remaining_time = stage_manager_->get_timer_transicio();
float progress = 1.0f - (remaining_time / total_time);
// Determine how many characters to show
size_t visible_chars;
if (typing_ratio > 0.0f && progress < typing_ratio) {
// Typewriter phase: show partial text
float typing_progress = progress / typing_ratio; // Normalize to 0.0-1.0
visible_chars = static_cast<size_t>(missatge.length() * typing_progress);
if (visible_chars == 0 && progress > 0.0f) {
visible_chars = 1; // Show at least 1 character after first frame
}
} else {
// Display phase: show complete text
// (Either after typing phase, or immediately if typing_ratio == 0.0)
visible_chars = missatge.length();
}
// Create partial message (substring for typewriter)
std::string partial_message = missatge.substr(0, visible_chars);
// ===================================================
// Calculate text width at base scale (using FULL message for position calculation)
float text_width_at_base = text_.get_text_width(missatge, escala_base, spacing);
// Auto-scale if text exceeds max width
float escala = (text_width_at_base <= max_width)
? escala_base
: max_width / text_width_at_base;
// Recalculate dimensions with final scale (using FULL message for centering)
float full_text_width = text_.get_text_width(missatge, escala, spacing);
float text_height = text_.get_text_height(escala);
// Calculate position as if FULL text was there (for fixed position typewriter)
float x = play_area.x + (play_area.w - full_text_width) / 2.0f;
float y = play_area.y + (play_area.h * 0.25f) - (text_height / 2.0f);
// Render only the partial message (typewriter effect)
Punt pos = {x, y};
text_.render(partial_message, pos, escala, spacing);
}
// ========================================
// Helper methods for 2-player support
// ========================================
Punt EscenaJoc::obtenir_punt_spawn(uint8_t player_id) const {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
float x_ratio = (player_id == 0)
? Defaults::Game::P1_SPAWN_X_RATIO
: Defaults::Game::P2_SPAWN_X_RATIO;
return {
zona.x + zona.w * x_ratio,
zona.y + zona.h * Defaults::Game::SPAWN_Y_RATIO
};
}
void EscenaJoc::disparar_bala(uint8_t player_id) {
// Verificar que el jugador está vivo
if (itocado_per_jugador_[player_id] > 0.0f) return;
if (!naus_[player_id].esta_viva()) return;
// Calcular posición en la punta de la nave
const Punt& ship_centre = naus_[player_id].get_centre();
float ship_angle = naus_[player_id].get_angle();
constexpr float LOCAL_TIP_X = 0.0f;
constexpr float LOCAL_TIP_Y = -12.0f;
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;
float tip_y = LOCAL_TIP_X * sin_a + LOCAL_TIP_Y * cos_a + ship_centre.y;
Punt posicio_dispar = {tip_x, tip_y};
// Buscar primera bala inactiva en el pool del jugador
int start_idx = player_id * 3; // P1=[0,1,2], P2=[3,4,5]
for (int i = start_idx; i < start_idx + 3; i++) {
if (!bales_[i].esta_activa()) {
bales_[i].disparar(posicio_dispar, ship_angle, player_id);
break;
}
}
}