feat(demo): attract mode amb pilot IA, escenaris curats i música contínua del títol

This commit is contained in:
2026-05-28 12:01:12 +02:00
parent 491992a4d7
commit c1956e0028
11 changed files with 404 additions and 27 deletions
+88 -23
View File
@@ -4,6 +4,7 @@
#include "game_scene.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdlib>
#include <ctime>
@@ -12,6 +13,7 @@
#include "core/audio/audio.hpp"
#include "core/entities/entity_loader.hpp"
#include "core/input/input.hpp"
#include "core/input/input_types.hpp"
#include "core/locale/locale.hpp"
#include "core/system/scene_context.hpp"
#include "core/system/service_menu.hpp"
@@ -29,6 +31,22 @@ using SceneManager::SceneContext;
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).
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<InputAction, 5> DEMO_EXIT_ACTIONS = {
InputAction::LEFT,
InputAction::RIGHT,
InputAction::THRUST,
InputAction::SHOOT,
InputAction::START};
} // namespace
GameScene::GameScene(SDLManager& sdl, SceneContext& context)
: sdl_(sdl),
context_(context),
@@ -139,7 +157,17 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
// Initialize stage manager
stage_manager_ = std::make_unique<StageSystem::StageManager>(stage_config_.get());
stage_manager_->init();
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());
context_.advanceDemoScenario();
stage_manager_->initDemo(DEMO_STAGE);
demo_timer_ = DEMO_DURATION;
} else {
stage_manager_->init();
}
// Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
stage_manager_->getWaveRunner().setShipPosition(&ships_[0].getCenter());
@@ -226,7 +254,12 @@ void GameScene::update(float delta_time) {
// mantener update() legible y reducir complejidad cognitiva.
stepPhysics(delta_time);
if (game_over_state_ == GameOverState::NONE) {
if (match_config_.mode == GameConfig::Mode::DEMO) {
// Mode demo (attract): salida por input/timeout/muerte + control del pilot.
if (stepDemo(delta_time)) {
return;
}
} else if (game_over_state_ == GameOverState::NONE) {
stepShootingInput();
stepMidGameJoin();
}
@@ -299,6 +332,56 @@ void GameScene::stepShootingInput() {
}
}
void GameScene::updateShipsControl(float delta_time) {
const bool DEMO = (match_config_.mode == GameConfig::Mode::DEMO);
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) {
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);
} else {
ships_[i].processInput(delta_time, i);
}
ships_[i].update(delta_time);
}
}
auto GameScene::stepDemo(float delta_time) -> bool {
// Qualsevol input trenca la demo i torna al títol (música intacta).
if (Input::get()->checkAnyPlayerAction(DEMO_EXIT_ACTIONS)) {
context_.setNextScene(SceneType::TITLE, Option::JUMP_TO_TITLE_MAIN);
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);
}
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);
}
return false;
}
void GameScene::endDemo() {
// No parem la música: title.ogg segueix sonant durant el cicle atrae.
context_.setNextScene(SceneType::LOGO);
}
void GameScene::stepMidGameJoin() {
// Permitir join solo durante PLAYING.
if (stage_manager_->getState() != StageSystem::EstatStage::PLAYING) {
@@ -496,13 +579,7 @@ void GameScene::runStageLevelStart(float delta_time) {
stage_manager_->update(delta_time);
// Ambas naves pueden moverse y disparar durante el intro.
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) {
ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time);
}
}
updateShipsControl(delta_time);
for (auto& bullet : bullets_) {
bullet.update(delta_time);
}
@@ -524,13 +601,7 @@ void GameScene::runStagePlaying(float delta_time) {
}
// Gameplay normal: ships activos + entidades + colisiones + efectos.
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) {
ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time);
}
}
updateShipsControl(delta_time);
auto ai_ctx = buildCollisionContext();
for (std::size_t i = 0; i < enemies_.size(); ++i) {
Systems::EnemyAi::tick(ai_ctx, enemies_[i], i, delta_time);
@@ -552,13 +623,7 @@ void GameScene::runStagePlaying(float delta_time) {
void GameScene::runStageLevelCompleted(float delta_time) {
stage_manager_->update(delta_time);
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) {
ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time);
}
}
updateShipsControl(delta_time);
for (auto& bullet : bullets_) {
bullet.update(delta_time);
}