// logo_scene.cpp - Implementació de l'escena logo // © 2026 JailDesigner #include "logo_scene.hpp" #include #include #include #include #include #include "core/audio/audio.hpp" #include "core/graphics/shape_loader.hpp" #include "core/input/input.hpp" #include "core/rendering/shape_renderer.hpp" #include "core/system/scene_context.hpp" // Using declarations per simplificar el codi using SceneManager::SceneContext; using SceneType = SceneContext::SceneType; using Option = SceneContext::Option; // Helper: calcular el progrés individual de una lletra // en función del progrés global (efecte seqüencial) static auto computeLetterProgress(size_t letra_index, size_t num_letras, float global_progress, float threshold) -> float { if (num_letras == 0) { return 1.0F; } // Calcular time per lletra float duration_per_letra = 1.0F / static_cast(num_letras); float step = threshold * duration_per_letra; float start = static_cast(letra_index) * step; float end = start + duration_per_letra; // Interpolar progrés if (global_progress < start) { return 0.0F; // Aún no ha començat } if (global_progress >= end) { return 1.0F; // Completament apareguda } return (global_progress - start) / (end - start); } LogoScene::LogoScene(SDLManager& sdl, SceneContext& context) : sdl_(sdl), context_(context), debris_manager_(std::make_unique(sdl.getRenderer())) { std::cout << "SceneType Logo: Inicialitzant...\n"; // Consumir opciones (LOGO no processa opciones actualment) auto option = context_.consumeOption(); (void)option; // Suprimir warning so_reproduit_.fill(false); // Inicialitzar seguiment de sons initLetters(); } LogoScene::~LogoScene() { // Aturar todos los sons y la música Audio::get()->stopAllSounds(); std::cout << "SceneType Logo: Sons parados\n"; } auto LogoScene::isFinished() const -> bool { return context_.nextScene() != SceneType::LOGO; } void LogoScene::handleEvent(const SDL_Event& event) { // La lógica de skip se decide en update() consultando el estado de Input; // aquí no hay eventos puntuales que procesar. (void)event; } void LogoScene::initLetters() { using namespace Graphics; // Llista de archivos .shp (A repetida para las dues A's) std::vector archivos = { "logo/letra_j.shp", "logo/letra_a.shp", "logo/letra_i.shp", "logo/letra_l.shp", "logo/letra_g.shp", "logo/letra_a.shp", "logo/letra_m.shp", "logo/letra_e.shp", "logo/letra_s.shp"}; // Pas 1: Carregar todas las formes i calcular amplades float ancho_total = 0.0F; for (const auto& file : archivos) { auto shape = ShapeLoader::load(file); if (!shape || !shape->isValid()) { std::cerr << "[LogoScene] Error carregant " << file << '\n'; continue; } // Calcular bounding box de la shape (trobar ancho) float min_x = FLT_MAX; float max_x = -FLT_MAX; for (const auto& prim : shape->get_primitives()) { for (const auto& point : prim.points) { min_x = std::min(min_x, point.x); max_x = std::max(max_x, point.x); } } float ancho_sin_escalar = max_x - min_x; // IMPORTANT: Escalar ancho i offset con ESCALA_FINAL // per que las posicions finals coincideixin con la mida real de las lletres float ancho = ancho_sin_escalar * ESCALA_FINAL; float offset_centre = (shape->getCenter().x - min_x) * ESCALA_FINAL; lletres_.push_back({shape, {.x = 0.0F, .y = 0.0F}, // Posición es calcularà después ancho, offset_centre}); ancho_total += ancho; } // Pas 2: Añadir espaiat entre lletres ancho_total += ESPAI_ENTRE_LLETRES * (lletres_.size() - 1); // Pas 3: Calcular posición inicial (centrat horitzontal) constexpr auto PANTALLA_ANCHO = static_cast(Defaults::Game::WIDTH); constexpr auto PANTALLA_ALTO = static_cast(Defaults::Game::HEIGHT); float x_inicial = (PANTALLA_ANCHO - ancho_total) / 2.0F; float y_centre = PANTALLA_ALTO / 2.0F; // Pas 4: Assignar posicions a cada lletra float x_actual = x_inicial; for (auto& lletra : lletres_) { // Posicionar el centro de la shape (shape_centre) en pantalla // Usar offset_centre en lloc de ancho/2 perquè shape_centre // pot no estar exactament al mig del bounding box lletra.position.x = x_actual + lletra.offset_centre; lletra.position.y = y_centre; // Avançar para següent lletra x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; } std::cout << "[LogoScene] " << lletres_.size() << " lletres carregades, ancho total: " << ancho_total << " px\n"; } void LogoScene::changeState(AnimationState nou_estat) { estat_actual_ = nou_estat; temps_estat_actual_ = 0.0F; // Reset time // Inicialitzar state de explosión if (nou_estat == AnimationState::EXPLOSION) { lletra_explosio_index_ = 0; temps_des_ultima_explosio_ = 0.0F; // Generar ordre aleatori de explosions ordre_explosio_.clear(); for (size_t i = 0; i < lletres_.size(); i++) { ordre_explosio_.push_back(i); } std::random_device rd; std::mt19937 g(rd()); std::shuffle(ordre_explosio_.begin(), ordre_explosio_.end(), g); } else if (nou_estat == AnimationState::POST_EXPLOSION) { Audio::get()->playMusic("title.ogg"); } std::cout << "[LogoScene] Canvi a state: " << static_cast(nou_estat) << "\n"; } auto LogoScene::allLettersComplete() const -> bool { // Cuando global_progress = 1.0, todas las lletres tenen letra_progress = 1.0 return temps_estat_actual_ >= DURACIO_ZOOM; } void LogoScene::updateExplosions(float delta_time) { temps_des_ultima_explosio_ += delta_time; // Comprovar si es el moment de explode la següent lletra if (temps_des_ultima_explosio_ >= DELAY_ENTRE_EXPLOSIONS) { if (lletra_explosio_index_ < lletres_.size()) { // Explotar lletra actual (en ordre aleatori) size_t index_actual = ordre_explosio_[lletra_explosio_index_]; const auto& lletra = lletres_[index_actual]; debris_manager_->explode( lletra.shape, // Forma a explode lletra.position, // Posición 0.0F, // Angle (sin rotación) ESCALA_FINAL, // Escala (lletres a scale final) VELOCITAT_EXPLOSIO, // Velocidad base 1.0F, // Brightness màxim (per defecte) {.x = 0.0F, .y = 0.0F} // Sin velocity (per defecte) ); std::cout << "[LogoScene] Explota lletra " << lletra_explosio_index_ << "\n"; // Passar a la següent lletra lletra_explosio_index_++; temps_des_ultima_explosio_ = 0.0F; } else { // Todas las lletres han explotat, transición a POST_EXPLOSION changeState(AnimationState::POST_EXPLOSION); } } } void LogoScene::update(float delta_time) { temps_estat_actual_ += delta_time; switch (estat_actual_) { case AnimationState::PRE_ANIMATION: if (temps_estat_actual_ >= DURACIO_PRE) { changeState(AnimationState::ANIMATION); } break; case AnimationState::ANIMATION: { // Reproduir so per cada lletra cuando comença a aparèixer float global_progress = std::min(temps_estat_actual_ / DURACIO_ZOOM, 1.0F); for (size_t i = 0; i < lletres_.size() && i < so_reproduit_.size(); i++) { if (!so_reproduit_[i]) { float letra_progress = computeLetterProgress( i, lletres_.size(), global_progress, THRESHOLD_LETRA); // Reproduir so cuando la lletra comença a aparèixer (progress > 0) if (letra_progress > 0.0F) { Audio::get()->playSound(Defaults::Sound::LOGO, Audio::Group::GAME); so_reproduit_[i] = true; } } } if (allLettersComplete()) { changeState(AnimationState::POST_ANIMATION); } break; } case AnimationState::POST_ANIMATION: if (temps_estat_actual_ >= DURACIO_POST_ANIMATION) { changeState(AnimationState::EXPLOSION); } break; case AnimationState::EXPLOSION: updateExplosions(delta_time); break; case AnimationState::POST_EXPLOSION: if (temps_estat_actual_ >= DURACIO_POST_EXPLOSION) { // Transición a pantalla de título context_.setNextScene(SceneType::TITLE); } break; } // Verificar botones de skip (SHOOT P1/P2) if (checkSkipButtonPressed()) { context_.setNextScene(SceneType::TITLE, Option::JUMP_TO_TITLE_MAIN); } // Actualitzar animaciones de debris debris_manager_->update(delta_time); } void LogoScene::draw() { // Director ha hecho el clear; aquí solo pintamos lo de la escena. // PRE_ANIMATION: Solo pantalla negra (no se pinta nada). if (estat_actual_ == AnimationState::PRE_ANIMATION) { return; } // ANIMATION o POST_ANIMATION: Dibuixar lletres con animación if (estat_actual_ == AnimationState::ANIMATION || estat_actual_ == AnimationState::POST_ANIMATION) { float global_progress = (estat_actual_ == AnimationState::ANIMATION) ? std::min(temps_estat_actual_ / DURACIO_ZOOM, 1.0F) : 1.0F; // POST: mantenir al 100% const Vec2 ORIGEN_ZOOM = {.x = ORIGEN_ZOOM_X, .y = ORIGEN_ZOOM_Y}; for (size_t i = 0; i < lletres_.size(); i++) { const auto& lletra = lletres_[i]; float letra_progress = computeLetterProgress( i, lletres_.size(), global_progress, THRESHOLD_LETRA); if (letra_progress <= 0.0F) { continue; } Vec2 pos_actual; pos_actual.x = ORIGEN_ZOOM.x + ((lletra.position.x - ORIGEN_ZOOM.x) * letra_progress); pos_actual.y = ORIGEN_ZOOM.y + ((lletra.position.y - ORIGEN_ZOOM.y) * letra_progress); float t = letra_progress; float ease_factor = 1.0F - ((1.0F - t) * (1.0F - t)); float current_scale = ESCALA_INICIAL + ((ESCALA_FINAL - ESCALA_INICIAL) * ease_factor); Rendering::render_shape( sdl_.getRenderer(), lletra.shape, pos_actual, 0.0F, current_scale, 1.0F); } } // EXPLOSION: Dibuixar solo lletres que aún no han explotat if (estat_actual_ == AnimationState::EXPLOSION) { // Crear conjunt de lletres ya explotades std::set explotades; for (size_t i = 0; i < lletra_explosio_index_; i++) { explotades.insert(ordre_explosio_[i]); } // Dibuixar solo lletres que NO han explotat for (size_t i = 0; i < lletres_.size(); i++) { if (!explotades.contains(i)) { const auto& lletra = lletres_[i]; Rendering::render_shape( sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, ESCALA_FINAL, 1.0F); } } } // POST_EXPLOSION: No draw lletres, solo debris (a baix) // Siempre draw debris (si n'hay de active) debris_manager_->draw(); } auto LogoScene::checkSkipButtonPressed() -> bool { return Input::get()->checkAnyPlayerAction(ARCADE_BUTTONS); }