diff --git a/source/core/system/scene_context.hpp b/source/core/system/scene_context.hpp index 93485cb..67ca5c5 100644 --- a/source/core/system/scene_context.hpp +++ b/source/core/system/scene_context.hpp @@ -71,7 +71,7 @@ namespace SceneManager { } // Nombre d'escenaris demo curats (cicle del attract mode). - static constexpr std::uint8_t DEMO_SCENARIO_COUNT = 3; + static constexpr std::uint8_t DEMO_SCENARIO_COUNT = 4; // Índex de l'escenari demo actual. Persisteix entre transicions (el // SceneContext el posseeix el Director), així cada entrada al mode demo diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 8d55e37..7e2b40f 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -32,11 +32,9 @@ using SceneType = SceneContext::SceneType; using Option = SceneContext::Option; namespace { - // Attract mode: duració màxima de la demo i marge perquè es vegi l'explosió - // de la nau abans de saltar al logo (menor que DEATH_DURATION=3s per evitar - // la seqüència de respawn/continue). + // Attract mode: durada fixa de la demo. Amb vides infinites, sempre dura + // això (les morts respawnegen); només input o aquest timeout la tallen. constexpr float DEMO_DURATION = 35.0F; - constexpr float DEMO_DEATH_LINGER = 2.0F; // Qualsevol d'aquestes accions trenca la demo i torna al títol. constexpr std::array DEMO_EXIT_ACTIONS = { @@ -160,10 +158,11 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) if (match_config_.mode == GameConfig::Mode::DEMO) { // Attract mode: arrencar directament en PLAYING a l'escenari curat // actual (partida "ja començada") i avançar l'índex perquè la pròxima - // demo mostri un escenari diferent. - const uint8_t DEMO_STAGE = Systems::Demo::scenarioStage(context_.demoScenarioIndex()); + // demo mostri un escenari diferent. El nombre de jugadors ja l'ha fixat + // TitleScene al match_config llegint el mateix escenari. + const Systems::Demo::Scenario SC = Systems::Demo::scenario(context_.demoScenarioIndex()); context_.advanceDemoScenario(); - stage_manager_->initDemo(DEMO_STAGE); + stage_manager_->initDemo(SC.stage); demo_timer_ = DEMO_DURATION; } else { stage_manager_->init(); @@ -339,10 +338,10 @@ void GameScene::updateShipsControl(float delta_time) { if (!ACTIU || hit_timer_per_player_[i] != 0.0F) { continue; } - // En demo, la P1 es mou amb el pilot IA (control calculat a stepDemo); - // la resta de casos llegeixen Input com sempre. - if (DEMO && i == 0) { - ships_[0].applyMovement(demo_ctrl_.left, demo_ctrl_.right, demo_ctrl_.thrust, delta_time); + // En demo, cada nau activa es mou amb el seu pilot IA (control calculat + // a stepDemo); la resta de casos llegeixen Input com sempre. + if (DEMO) { + ships_[i].applyMovement(demo_ctrls_[i].left, demo_ctrls_[i].right, demo_ctrls_[i].thrust, delta_time); } else { ships_[i].processInput(delta_time, i); } @@ -357,22 +356,30 @@ auto GameScene::stepDemo(float delta_time) -> bool { return true; } - // En morir la nau, escurçar el timer perquè es vegi l'explosió i després - // saltar al logo (sense passar per CONTINUE/GAME_OVER ni parar la música). - if (hit_timer_per_player_[0] > 0.0F) { - demo_timer_ = std::min(demo_timer_, DEMO_DEATH_LINGER); - } - + // Vides infinites: la demo dura sempre el temps fix; en morir, stepDeathSequence + // respawneja (no acaba ni passa per CONTINUE/GAME_OVER). demo_timer_ -= delta_time; if (demo_timer_ <= 0.0F) { endDemo(); return true; } - // Control del pilot per al frame; el disparo el dispara GameScene. - demo_ctrl_ = demo_pilot_.compute(ships_[0], enemies_, Defaults::Zones::PLAYAREA, delta_time); - if (demo_ctrl_.shoot) { - fireBullet(0); + // Control de cada nau activa per al frame; el disparo el dispara GameScene. + for (uint8_t i = 0; i < 2; i++) { + const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active; + if (!ACTIU || hit_timer_per_player_[i] != 0.0F) { + demo_ctrls_[i] = {}; // nau inactiva/morta: sense control + continue; + } + demo_ctrls_[i] = demo_pilots_[i].compute( + ships_[i], + enemies_, + bullets_, + Defaults::Zones::PLAYAREA, + delta_time); + if (demo_ctrls_[i].shoot) { + fireBullet(i); + } } return false; } @@ -491,6 +498,15 @@ void GameScene::stepDeathSequence(float delta_time) { } // *** PHASE 3: RESPAWN OR GAME OVER *** + // Mode demo: vides infinites. Respawn sempre, sense decrementar vides ni + // passar mai per CONTINUE/GAME_OVER — la demo dura el seu temps fix. + if (match_config_.mode == GameConfig::Mode::DEMO) { + Vec2 spawn_pos = getSpawnPoint(i); + ships_[i].init(&spawn_pos, /*activar_invulnerabilitat=*/true); + hit_timer_per_player_[i] = 0.0F; + continue; + } + lives_per_player_[i]--; if (lives_per_player_[i] > 0) { Vec2 spawn_pos = getSpawnPoint(i); diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index a5fbcc6..e2d2cc4 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -102,10 +102,10 @@ class GameScene final : public Scene { // Control de sons de animación INIT_HUD bool init_hud_rect_sound_played_{false}; // Flag para evitar repetir sonido del rectángulo - // Attract mode (mode DEMO): un pilot IA controla la nau P1. - Systems::Demo::DemoPilot demo_pilot_; - Systems::Demo::Control demo_ctrl_{}; // Control del pilot per al frame actual - float demo_timer_{0.0F}; // Temps restant de la demo (→ LOGO en esgotar-se) + // Attract mode (mode DEMO): un pilot IA per nau activa (1 o 2 jugadors). + std::array demo_pilots_; + std::array demo_ctrls_{}; // Control per nau al frame actual + float demo_timer_{0.0F}; // Temps restant de la demo (→ LOGO) // Funciones privades // bullet_velocity: velocitat de la bala que ha causat la mort (Vec2{} si no @@ -144,7 +144,7 @@ class GameScene final : public Scene { void stepPhysics(float delta_time); void stepShootingInput(); void stepMidGameJoin(); - // Mueve las naves activas: en mode DEMO la P1 usa el pilot IA (demo_ctrl_), + // Mueve las naves activas: en mode DEMO cada nau usa su pilot IA (demo_ctrls_), // el resto usa processInput (Input). Compartido por los 3 estados jugables. void updateShipsControl(float delta_time); // Mode DEMO: gestiona salida (input→título, timeout/muerte→logo) y calcula diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 1464444..d8a1968 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -20,6 +20,7 @@ #include "core/rendering/shape_renderer.hpp" #include "core/system/scene_context.hpp" #include "core/system/service_menu.hpp" +#include "game/systems/demo_pilot.hpp" #include "project.h" using SceneManager::SceneContext; @@ -359,9 +360,12 @@ void TitleScene::update(float delta_time) { idle_timer_ += delta_time; } if (idle_timer_ >= TITLE_DEMO_TIMEOUT) { + // L'escenari curat (mateix índex que llegirà GameScene) decideix + // quants jugadors IA hi ha. No avancem l'índex ací: ho fa GameScene. + const Systems::Demo::Scenario SC = Systems::Demo::scenario(context_.demoScenarioIndex()); GameConfig::MatchConfig demo_cfg; demo_cfg.player1_active = true; - demo_cfg.player2_active = false; + demo_cfg.player2_active = (SC.players >= 2); demo_cfg.mode = GameConfig::Mode::DEMO; context_.setMatchConfig(demo_cfg); context_.setNextScene(SceneType::GAME); diff --git a/source/game/systems/demo_pilot.cpp b/source/game/systems/demo_pilot.cpp index 2078e74..468907e 100644 --- a/source/game/systems/demo_pilot.cpp +++ b/source/game/systems/demo_pilot.cpp @@ -32,6 +32,10 @@ namespace Systems::Demo { constexpr float WALL_BIAS = 0.6F; // peso del empuje hacia el centro al esquivar + // Esquiva de balas enemigas. + constexpr float DODGE_SCAN_RADIUS = 190.0F; // px: distancia a la que reacciona + constexpr float DODGE_HEADING_MIN = 0.25F; // dot mínimo: la bala viene hacia la nave + // [-1, 1] aleatorio (estética: jitter de apuntado; no afecta a la simulación). auto randSigned() -> float { return (static_cast(std::rand()) / static_cast(RAND_MAX) * 2.0F) - 1.0F; @@ -72,10 +76,71 @@ namespace Systems::Demo { return best; } + struct BulletThreat { + bool found{false}; + Vec2 position{}; + Vec2 velocity{}; + }; + + // Bala enemiga más cercana que viene hacia la nave (dentro del radio de + // reacción). Solo balas de enemic (owner_id >= ENEMY_OWNER_BASE). + auto findBulletThreat(const Vec2& ship_pos, + const std::array(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets) + -> BulletThreat { + BulletThreat threat; + float best_d2 = 0.0F; + for (const auto& bullet : bullets) { + if (!bullet.isActive() || + bullet.getOwnerId() < Defaults::Entities::ENEMY_OWNER_BASE) { + continue; + } + const Vec2 BPOS = bullet.getCenter(); + const Vec2 TO_SHIP = ship_pos - BPOS; + const float D2 = TO_SHIP.lengthSquared(); + if (D2 > DODGE_SCAN_RADIUS * DODGE_SCAN_RADIUS) { + continue; + } + const Vec2 BVEL = bullet.getBody().velocity; + if (BVEL.lengthSquared() < 1.0F) { + continue; + } + // ¿La bala se dirige hacia la nave? + if (BVEL.normalized().dot(TO_SHIP.normalized()) < DODGE_HEADING_MIN) { + continue; + } + if (!threat.found || D2 < best_d2) { + threat.found = true; + best_d2 = D2; + threat.position = BPOS; + threat.velocity = BVEL; + } + } + return threat; + } + + // Error angular (rad, [-PI,PI]) entre la nariz de la nave y desired_dir, + // con el jitter de apuntado aplicado. La nariz apunta hacia (angle - PI/2). + auto steerError(const Ship& ship, const Vec2& desired_dir, float jitter) -> float { + const float DESIRED = std::atan2(desired_dir.y, desired_dir.x) + jitter; + const float NOSE = ship.getAngle() - (PI / 2.0F); + return wrapPi(DESIRED - NOSE); + } + + // Setea rotación según el error (RIGHT incrementa ship.angle; LEFT lo + // decrementa), con zona muerta para no oscilar. + void applyRotation(Control& ctrl, float error) { + if (error > ROTATE_DEADZONE) { + ctrl.right = true; + } else if (error < -ROTATE_DEADZONE) { + ctrl.left = true; + } + } + } // namespace auto DemoPilot::compute(const Ship& ship, const std::array& enemies, + const std::array(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets, const SDL_FRect& play_area, float delta_time) -> Control { Control ctrl; @@ -94,6 +159,26 @@ namespace Systems::Demo { } const Vec2 SHIP_POS = ship.getCenter(); + const Vec2 PLAY_CENTRE{ + .x = play_area.x + (play_area.w / 2.0F), + .y = play_area.y + (play_area.h / 2.0F)}; + const Vec2 TO_CENTRE = (PLAY_CENTRE - SHIP_POS).normalized(); + + // Prioridad 1: esquivar una bala enemiga entrante. Se mueve perpendicular + // a la trayectoria de la bala (con sesgo al centro) y no dispara. + const BulletThreat THREAT = findBulletThreat(SHIP_POS, bullets); + if (THREAT.found) { + const Vec2 BV = THREAT.velocity.normalized(); + Vec2 perp{.x = -BV.y, .y = BV.x}; + if ((SHIP_POS - THREAT.position).dot(perp) < 0.0F) { + perp = -perp; // hacia el lado en que ya está la nave + } + const Vec2 ESCAPE = (perp + (TO_CENTRE * WALL_BIAS)).normalized(); + applyRotation(ctrl, steerError(ship, ESCAPE, aim_jitter_)); + ctrl.thrust = true; + return ctrl; + } + const Nearest TARGET = findNearest(SHIP_POS, enemies); target_idx_ = TARGET.index; @@ -103,10 +188,6 @@ namespace Systems::Demo { return ctrl; } - // Centro de la zona de juego (sesgo anti-pared). - const Vec2 PLAY_CENTRE{ - .x = play_area.x + (play_area.w / 2.0F), - .y = play_area.y + (play_area.h / 2.0F)}; const bool NEAR_WALL = SHIP_POS.x < play_area.x + WALL_MARGIN || SHIP_POS.x > play_area.x + play_area.w - WALL_MARGIN || @@ -116,10 +197,8 @@ namespace Systems::Demo { Vec2 desired_dir; const bool DANGER = TARGET.distance < DANGER_RADIUS; if (DANGER) { - // Esquiva: alejarse del enemigo, con sesgo hacia el centro para no - // quedar atrapada contra la pared. Empuje activo para crear espacio. + // Prioridad 2: alejarse del enemigo pegado, con sesgo al centro. const Vec2 AWAY = (SHIP_POS - TARGET.center).normalized(); - const Vec2 TO_CENTRE = (PLAY_CENTRE - SHIP_POS).normalized(); desired_dir = (AWAY + (TO_CENTRE * WALL_BIAS)).normalized(); ctrl.thrust = true; } else { @@ -128,23 +207,13 @@ namespace Systems::Demo { desired_dir = (PREDICTED - SHIP_POS).normalized(); } - // Ángulo deseado de la nariz; la nariz apunta hacia (angle - PI/2). - float desired_angle = std::atan2(desired_dir.y, desired_dir.x) + aim_jitter_; - const float NOSE_ANGLE = ship.getAngle() - (PI / 2.0F); - const float ERROR = wrapPi(desired_angle - NOSE_ANGLE); - - // RIGHT incrementa ship.angle (→ nariz); LEFT lo decrementa. - if (ERROR > ROTATE_DEADZONE) { - ctrl.right = true; - } else if (ERROR < -ROTATE_DEADZONE) { - ctrl.left = true; - } + const float ERROR = steerError(ship, desired_dir, aim_jitter_); + applyRotation(ctrl, ERROR); if (!DANGER) { // Acercarse si el objetivo está lejos (mantiene la nave cazando), // pero no empujar de cara a una pared. - const bool FACING_WALL = NEAR_WALL && - (PLAY_CENTRE - SHIP_POS).normalized().dot(desired_dir) < 0.0F; + const bool FACING_WALL = NEAR_WALL && TO_CENTRE.dot(desired_dir) < 0.0F; if (TARGET.distance > APPROACH_RADIUS && !FACING_WALL && std::fabs(ERROR) < FIRE_TOLERANCE) { ctrl.thrust = true; diff --git a/source/game/systems/demo_pilot.hpp b/source/game/systems/demo_pilot.hpp index c1ad50f..c1bb816 100644 --- a/source/game/systems/demo_pilot.hpp +++ b/source/game/systems/demo_pilot.hpp @@ -14,23 +14,36 @@ #include #include +#include #include +#include "core/defaults/entities.hpp" #include "core/system/scene_context.hpp" #include "game/constants.hpp" +#include "game/entities/bullet.hpp" #include "game/entities/enemy.hpp" #include "game/entities/ship.hpp" namespace Systems::Demo { - // Stages de arranque de cada escenario curado (se ciclan en attract mode). - // Elegidos por variedad visual: pinwheels, stars, squares/mix. - inline constexpr std::array - SCENARIO_STAGES = {2, 5, 7}; + // Escenari curat del attract mode: stage de arranque + nombre de naus IA. + // Tots inclouen estrelles (que disparen) per donar feina al pilot. + struct Scenario { + std::uint8_t stage; // stage_id de stages.yaml on arrenca la demo + std::uint8_t players; // 1 o 2 naus controlades per la IA + }; - // Stage de arranque para el índice de escenario dado. - [[nodiscard]] inline auto scenarioStage(std::uint8_t index) -> std::uint8_t { - return SCENARIO_STAGES.at(index % SCENARIO_STAGES.size()); + inline constexpr std::array + SCENARIOS = {{ + {.stage = 5, .players = 1}, // estrelles + mix, 1 jugador + {.stage = 8, .players = 1}, // pressió alta, 1 jugador + {.stage = 6, .players = 2}, // densitat alta, 2 jugadors (foc amic ON) + {.stage = 10, .players = 2}, // repte final, 2 jugadors (foc amic ON) + }}; + + // Escenari per a l'índex donat (cíclic). + [[nodiscard]] inline auto scenario(std::uint8_t index) -> Scenario { + return SCENARIOS.at(index % SCENARIOS.size()); } // Control de la nave para un frame. @@ -45,6 +58,7 @@ namespace Systems::Demo { public: [[nodiscard]] auto compute(const Ship& ship, const std::array& enemies, + const std::array(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets, const SDL_FRect& play_area, float delta_time) -> Control;