// joc_asteroides.cpp - Implementació de la lògica del joc // © 1999 Visente i Sergi (versió Pascal) // © 2025 Port a C++20 amb SDL3 #include "joc_asteroides.hpp" #include #include #include #include JocAsteroides::JocAsteroides(SDL_Renderer* renderer) : renderer_(renderer), itocado_(0) { } void JocAsteroides::inicialitzar() { // Inicialitzar generador de números aleatoris // Basat en el codi Pascal original: line 376 std::srand(static_cast(std::time(nullptr))); // Inicialització de la nau (triangle) // Basat en el codi Pascal original: lines 380-384 nau_.p1.r = 12.0f; nau_.p1.angle = 3.0f * Constants::PI / 2.0f; // Apunta amunt (270°) nau_.p2.r = 12.0f; nau_.p2.angle = Constants::PI / 4.0f; // 45° nau_.p3.r = 12.0f; nau_.p3.angle = (3.0f * Constants::PI) / 4.0f; // 135° nau_.angle = 0.0f; nau_.centre.x = 320.0f; nau_.centre.y = 240.0f; nau_.velocitat = 0.0f; // Inicialitzar estat de col·lisió itocado_ = 0; // Inicialitzar enemics (ORNIs) // Basat en el codi Pascal original: line 386 for (int i = 0; i < Constants::MAX_ORNIS; i++) { // Crear pentàgon regular (5 costats, radi 20) crear_poligon_regular(orni_[i], 5, 20.0f); // Posició aleatòria dins de l'àrea de joc orni_[i].centre.x = static_cast((std::rand() % 580) + 30); // 30-610 orni_[i].centre.y = static_cast((std::rand() % 420) + 30); // 30-450 // Angle aleatori orni_[i].angle = (std::rand() % 360) * Constants::PI / 180.0f; // Està actiu orni_[i].esta = true; } // Inicialitzar bales // Basat en el codi Pascal original: inicialment inactives for (int i = 0; i < Constants::MAX_BALES; i++) { // Crear pentàgon petit (5 costats, radi 5) crear_poligon_regular(bales_[i], 5, 5.0f); // Inicialment inactiva bales_[i].esta = false; } } void JocAsteroides::actualitzar(float delta_time) { // Actualització de la física de la nau (TIME-BASED) // Basat en el codi Pascal original: lines 394-417 // Convertit a time-based per ser independent del framerate // Constants de física (convertides des del Pascal original a ~20 FPS) constexpr float ROTATION_SPEED = 3.14f; // rad/s (0.157 rad/frame × 20 = 3.14 rad/s, ~180°/s) constexpr float ACCELERATION = 400.0f; // px/s² (0.2 u/frame × 20 × 100 = 400 px/s²) constexpr float MAX_VELOCITY = 120.0f; // px/s (6 u/frame × 20 = 120 px/s) constexpr float FRICTION = 20.0f; // px/s² (0.1 u/frame × 20 × 10 = 20 px/s²) // Obtenir estat actual del teclat (no events, sinó estat continu) const bool* keyboard_state = SDL_GetKeyboardState(nullptr); // Processar input continu (com teclapuls() del Pascal original) if (keyboard_state[SDL_SCANCODE_RIGHT]) { nau_.angle += ROTATION_SPEED * delta_time; } if (keyboard_state[SDL_SCANCODE_LEFT]) { nau_.angle -= ROTATION_SPEED * delta_time; } if (keyboard_state[SDL_SCANCODE_UP]) { if (nau_.velocitat < MAX_VELOCITY) { nau_.velocitat += ACCELERATION * delta_time; if (nau_.velocitat > MAX_VELOCITY) { nau_.velocitat = MAX_VELOCITY; } } } // Calcular nova posició basada en velocitat i angle // S'usa (angle - PI/2) perquè angle=0 apunte cap amunt, no cap a la dreta // velocitat està en px/s, així que multipliquem per delta_time float dy = (nau_.velocitat * delta_time) * std::sin(nau_.angle - Constants::PI / 2.0f) + nau_.centre.y; float dx = (nau_.velocitat * delta_time) * std::cos(nau_.angle - Constants::PI / 2.0f) + nau_.centre.x; // Boundary checking - només actualitzar si dins dels marges // Acumulació directa amb precisió subpíxel if (dy > Constants::MARGE_DALT && dy < Constants::MARGE_BAIX) { nau_.centre.y = dy; } if (dx > Constants::MARGE_ESQ && dx < Constants::MARGE_DRET) { nau_.centre.x = dx; } // Fricció - desacceleració gradual (time-based) if (nau_.velocitat > 0.1f) { nau_.velocitat -= FRICTION * delta_time; if (nau_.velocitat < 0.0f) { nau_.velocitat = 0.0f; } } // DEBUG: Mostrar info de la nau (temporal) static float time_accumulator = 0.0f; time_accumulator += delta_time; if (time_accumulator >= 1.0f) { // Cada segon std::cout << "Nau: pos(" << nau_.centre.x << "," << nau_.centre.y << ") vel=" << static_cast(nau_.velocitat) << " px/s" << " angle=" << static_cast(nau_.angle * 180.0f / Constants::PI) << "°" << " dt=" << static_cast(delta_time * 1000.0f) << "ms" << std::endl; time_accumulator -= 1.0f; } // Actualitzar moviment i rotació dels enemics (ORNIs) // Basat en el codi Pascal original: lines 429-432 for (auto& enemy : orni_) { if (enemy.esta) { // Moviment autònom (Fase 8) mou_orni(enemy, delta_time); // Rotació visual (time-based: drotacio està en rad/s) enemy.rotacio += enemy.drotacio * delta_time; } } // Actualitzar moviment de bales (Fase 9) for (auto& bala : bales_) { if (bala.esta) { mou_bales(bala, delta_time); } } } void JocAsteroides::dibuixar() { // Dibuixar la nau si no està en seqüència de mort if (itocado_ == 0) { // Escalar velocitat per l'efect visual (120 px/s → ~6 px d'efecte) // El codi Pascal original sumava velocitat (0-6) al radi per donar // sensació de "empenta". Ara velocitat està en px/s (0-120). float velocitat_visual = nau_.velocitat / 20.0f; rota_tri(nau_, nau_.angle, velocitat_visual, true); } // Dibuixar ORNIs (enemics) // Basat en el codi Pascal original: lines 429-432 for (const auto& enemy : orni_) { if (enemy.esta) { rota_pol(enemy, enemy.rotacio, true); } } // Dibuixar bales (Fase 9) for (const auto& bala : bales_) { if (bala.esta) { // Dibuixar com a pentàgon petit, sense rotació visual (sempre mateix angle) rota_pol(bala, 0.0f, true); } } // TODO: Dibuixar marges (Fase 11) } void JocAsteroides::processar_input(const SDL_Event& event) { // Processament d'input per events puntuals (no continus) // L'input continu (fletxes) es processa en actualitzar() amb SDL_GetKeyboardState() if (event.type == SDL_EVENT_KEY_DOWN) { switch (event.key.key) { case SDLK_SPACE: // Disparar (Fase 9) // Basat en el codi Pascal original: crear bala en posició de la nau // El joc original només permetia 1 bala activa alhora // Buscar primera bala inactiva for (auto& bala : bales_) { if (!bala.esta) { // Activar bala bala.esta = true; // Posició inicial = centre de la nau bala.centre.x = nau_.centre.x; bala.centre.y = nau_.centre.y; // Angle = angle de la nau (dispara en la direcció que apunta) bala.angle = nau_.angle; // Velocitat alta (el joc Pascal original usava 7 px/frame) // 7 px/frame × 20 FPS = 140 px/s bala.velocitat = 140.0f; // Només una bala alhora (com el joc original) break; } } break; default: break; } } } // Funcions utilitàries - Geometria i matemàtiques // Basades en el codi Pascal original float JocAsteroides::modul(const Punt& p) const { // Càlcul de la magnitud d'un vector: sqrt(x² + y²) return std::sqrt(static_cast(p.x * p.x + p.y * p.y)); } void JocAsteroides::diferencia(const Punt& o, const Punt& d, Punt& p) const { // Resta de vectors (origen - destí) p.x = o.x - d.x; p.y = o.y - d.y; } int JocAsteroides::distancia(const Punt& o, const Punt& d) const { // Distància entre dos punts Punt p; diferencia(o, d, p); return static_cast(std::round(modul(p))); } float JocAsteroides::angle_punt(const Punt& p) const { // Càlcul de l'angle d'un punt (arctan) if (p.y != 0) { return std::atan(static_cast(p.x) / static_cast(p.y)); } return 0.0f; } void JocAsteroides::crear_poligon_regular(Poligon& pol, uint8_t n, float r) { // Crear un polígon regular amb n costats i radi r // Distribueix els punts uniformement al voltant d'un cercle float interval = 2.0f * Constants::PI / n; float act = 0.0f; for (uint8_t i = 0; i < n; i++) { pol.ipuntx[i].r = r; pol.ipuntx[i].angle = act; act += interval; } // Inicialitzar propietats del polígon pol.centre.x = 320.0f; pol.centre.y = 200.0f; pol.angle = 0.0f; // Convertir velocitat de px/frame a px/s: 2 px/frame × 20 FPS = 40 px/s pol.velocitat = static_cast(Constants::VELOCITAT) * 20.0f; pol.n = n; // Convertir rotació de rad/frame a rad/s: 0.0785 rad/frame × 20 FPS = 1.57 rad/s (~90°/s) pol.drotacio = 0.078539816f * 20.0f; pol.rotacio = 0.0f; pol.esta = true; } bool JocAsteroides::linea(int x1, int y1, int x2, int y2, bool dibuixar) { // Algorisme de Bresenham per dibuixar línies // Basat en el codi Pascal original // Helper function: retorna el signe d'un nombre auto sign = [](int x) -> int { if (x < 0) return -1; if (x > 0) return 1; return 0; }; // Variables per a l'algorisme (no utilitzades fins Fase 10 - detecció de col·lisions) // int x = x1, y = y1; // int xs = x2 - x1; // int ys = y2 - y1; // int xm = sign(xs); // int ym = sign(ys); // xs = std::abs(xs); // ys = std::abs(ys); // Suprimir warning de variable no usada (void)sign; // Detecció de col·lisió (TODO per Fase 10) // El codi Pascal original llegia pixels del framebuffer bit-packed // i comptava col·lisions. Per ara, usem SDL_RenderDrawLine i retornem false. bool colisio = false; // Dibuixar amb SDL3 (més eficient que Bresenham píxel a píxel) if (dibuixar && renderer_) { SDL_SetRenderDrawColor(renderer_, 255, 255, 255, 255); // Blanc SDL_RenderLine(renderer_, static_cast(x1), static_cast(y1), static_cast(x2), static_cast(y2)); } // Algorisme de Bresenham original (conservat per a futura detecció de col·lisió) /* if (xs > ys) { // Línia plana (<45 graus) int count = -(xs / 2); while (x != x2) { count = count + ys; x = x + xm; if (count > 0) { y = y + ym; count = count - xs; } // Aquí aniria la detecció de col·lisió píxel a píxel } } else { // Línia pronunciada (>=45 graus) int count = -(ys / 2); while (y != y2) { count = count + xs; y = y + ym; if (count > 0) { x = x + xm; count = count - ys; } // Aquí aniria la detecció de col·lisió píxel a píxel } } */ return colisio; } void JocAsteroides::rota_tri(const Triangle& tri, float angul, float velocitat, bool dibuixar) { // Rotar i dibuixar triangle (nau) // Conversió de coordenades polars a cartesianes amb rotació // Basat en el codi Pascal original: lines 271-284 // Convertir cada punt polar a cartesià // x = (r + velocitat) * cos(angle_punt + angle_nau) + centre.x // y = (r + velocitat) * sin(angle_punt + angle_nau) + centre.y int x1 = static_cast(std::round( (tri.p1.r + velocitat) * std::cos(tri.p1.angle + angul) )) + tri.centre.x; int y1 = static_cast(std::round( (tri.p1.r + velocitat) * std::sin(tri.p1.angle + angul) )) + tri.centre.y; int x2 = static_cast(std::round( (tri.p2.r + velocitat) * std::cos(tri.p2.angle + angul) )) + tri.centre.x; int y2 = static_cast(std::round( (tri.p2.r + velocitat) * std::sin(tri.p2.angle + angul) )) + tri.centre.y; int x3 = static_cast(std::round( (tri.p3.r + velocitat) * std::cos(tri.p3.angle + angul) )) + tri.centre.x; int y3 = static_cast(std::round( (tri.p3.r + velocitat) * std::sin(tri.p3.angle + angul) )) + tri.centre.y; // Dibuixar les 3 línies que formen el triangle linea(x1, y1, x2, y2, dibuixar); linea(x1, y1, x3, y3, dibuixar); linea(x3, y3, x2, y2, dibuixar); } void JocAsteroides::rota_pol(const Poligon& pol, float angul, bool dibuixar) { // Rotar i dibuixar polígon (enemics i bales) // Conversió de coordenades polars a cartesianes amb rotació // Basat en el codi Pascal original: lines 286-296 // Array temporal per emmagatzemar punts convertits a cartesianes std::array xy; // Convertir cada punt polar a cartesià for (uint8_t i = 0; i < pol.n; i++) { xy[i].x = static_cast(std::round( pol.ipuntx[i].r * std::cos(pol.ipuntx[i].angle + angul) )) + pol.centre.x; xy[i].y = static_cast(std::round( pol.ipuntx[i].r * std::sin(pol.ipuntx[i].angle + angul) )) + pol.centre.y; } // Dibuixar línies entre punts consecutius for (uint8_t i = 0; i < pol.n - 1; i++) { linea(xy[i].x, xy[i].y, xy[i + 1].x, xy[i + 1].y, dibuixar); } // Tancar el polígon (últim punt → primer punt) linea(xy[pol.n - 1].x, xy[pol.n - 1].y, xy[0].x, xy[0].y, dibuixar); } void JocAsteroides::mou_orni(Poligon& orni, float delta_time) { // Moviment autònom d'ORNI (enemic pentàgon) // Basat en el codi Pascal original: procedure mou_orni // Cambio aleatori d'angle (5% probabilitat per crida) // En el Pascal original: if (random<0.05) then orni.angle:=random*2*pi float random_val = static_cast(std::rand()) / static_cast(RAND_MAX); if (random_val < 0.05f) { // Assignar un angle completament aleatori (0-360°) orni.angle = (static_cast(std::rand()) / static_cast(RAND_MAX)) * 2.0f * Constants::PI; } // Calcular nova posició (moviment polar time-based) // velocitat ja està en px/s (40 px/s), només cal multiplicar per delta_time float velocitat_efectiva = orni.velocitat * delta_time; // Calcular desplaçament (angle-PI/2 perquè angle=0 apunta amunt) float dy = velocitat_efectiva * std::sin(orni.angle - Constants::PI / 2.0f); float dx = velocitat_efectiva * std::cos(orni.angle - Constants::PI / 2.0f); // Acumulació directa amb precisió subpíxel orni.centre.y += dy; orni.centre.x += dx; // Boundary checking amb rebot (reflexió d'angle) // Si toca paret esquerra/dreta: angle = PI - angle if (orni.centre.x < Constants::MARGE_ESQ || orni.centre.x > Constants::MARGE_DRET) { orni.angle = Constants::PI - orni.angle; } // Si toca paret dalt/baix: angle = 2*PI - angle if (orni.centre.y < Constants::MARGE_DALT || orni.centre.y > Constants::MARGE_BAIX) { orni.angle = 2.0f * Constants::PI - orni.angle; } // Nota: La rotació visual (orni.rotacio += orni.drotacio) ja es fa a actualitzar() } void JocAsteroides::mou_bales(Poligon& bala, float delta_time) { // Moviment rectilini de la bala // Basat en el codi Pascal original: procedure mou_bales // Calcular nova posició (moviment polar time-based) // velocitat ja està en px/s (140 px/s), només cal multiplicar per delta_time float velocitat_efectiva = bala.velocitat * delta_time; // Calcular desplaçament (angle-PI/2 perquè angle=0 apunta amunt) float dy = velocitat_efectiva * std::sin(bala.angle - Constants::PI / 2.0f); float dx = velocitat_efectiva * std::cos(bala.angle - Constants::PI / 2.0f); // Acumulació directa amb precisió subpíxel bala.centre.y += dy; bala.centre.x += dx; // Desactivar si surt dels marges (no rebota com els ORNIs) if (bala.centre.x < Constants::MARGE_ESQ || bala.centre.x > Constants::MARGE_DRET || bala.centre.y < Constants::MARGE_DALT || bala.centre.y > Constants::MARGE_BAIX) { bala.esta = false; } } void JocAsteroides::tocado() { // TODO: Implementar seqüència de mort }