feat(demo): attract mode amb pilot IA, escenaris curats i música contínua del títol
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "game/stage_system/stage_config.hpp"
|
||||
#include "game/stage_system/stage_manager.hpp"
|
||||
#include "game/systems/collision_system.hpp"
|
||||
#include "game/systems/demo_pilot.hpp"
|
||||
#include "game/systems/init_hud_animator.hpp"
|
||||
|
||||
// Game over state machine
|
||||
@@ -101,6 +102,11 @@ 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)
|
||||
|
||||
// Funciones privades
|
||||
// bullet_velocity: velocitat de la bala que ha causat la mort (Vec2{} si no
|
||||
// ve d'una bala). Es passa al debris perquè els fragments volin en direcció
|
||||
@@ -138,6 +144,13 @@ 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_),
|
||||
// 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
|
||||
// el control + disparo del pilot. Devuelve true si la escena transiciona.
|
||||
[[nodiscard]] auto stepDemo(float delta_time) -> bool;
|
||||
void endDemo();
|
||||
// Devuelven true si el frame debe salir tras esta sección.
|
||||
[[nodiscard]] auto stepContinueScreen(float delta_time) -> bool;
|
||||
[[nodiscard]] auto stepGameOver(float delta_time) -> bool;
|
||||
|
||||
@@ -94,7 +94,15 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
||||
}
|
||||
|
||||
TitleScene::~TitleScene() {
|
||||
Audio::get()->stopMusic();
|
||||
// Attract mode: si saltem a la demo, NO parem la música — ha de seguir
|
||||
// sonant durant tot el cicle TÍTOL→DEMO→LOGO→TÍTOL. La resta de sortides
|
||||
// (partida normal, EXIT) sí paren.
|
||||
const bool ENTERING_DEMO =
|
||||
context_.nextScene() == SceneType::GAME &&
|
||||
context_.getMatchConfig().mode == GameConfig::Mode::DEMO;
|
||||
if (!ENTERING_DEMO) {
|
||||
Audio::get()->stopMusic();
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::initTitle() {
|
||||
@@ -340,6 +348,25 @@ void TitleScene::update(float delta_time) {
|
||||
handleSkipInput();
|
||||
handleStartInput();
|
||||
}
|
||||
|
||||
// Attract mode: al state MAIN, acumular inactivitat; qualsevol botó
|
||||
// arcade la reseteja. En esgotar el timeout, saltar a la demo (mode DEMO,
|
||||
// P1 actiu) sense fer fadeOut de la música (a diferència del START real).
|
||||
if (current_state_ == TitleState::MAIN && !INPUT_BLOCKED) {
|
||||
if (Input::get()->checkAnyPlayerAction(ARCADE_BUTTONS, Input::ALLOW_REPEAT)) {
|
||||
idle_timer_ = 0.0F;
|
||||
} else {
|
||||
idle_timer_ += delta_time;
|
||||
}
|
||||
if (idle_timer_ >= TITLE_DEMO_TIMEOUT) {
|
||||
GameConfig::MatchConfig demo_cfg;
|
||||
demo_cfg.player1_active = true;
|
||||
demo_cfg.player2_active = false;
|
||||
demo_cfg.mode = GameConfig::Mode::DEMO;
|
||||
context_.setMatchConfig(demo_cfg);
|
||||
context_.setNextScene(SceneType::GAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::updateStarfieldFadeInState(float delta_time) {
|
||||
|
||||
@@ -79,6 +79,7 @@ class TitleScene final : public Scene {
|
||||
void drawFlashes();
|
||||
TitleState current_state_{TitleState::STARFIELD_FADE_IN};
|
||||
float temps_acumulat_{0.0F};
|
||||
float idle_timer_{0.0F}; // Attract mode: inactivitat acumulada al state MAIN
|
||||
|
||||
std::vector<LogoLetter> letters_orni_;
|
||||
std::vector<LogoLetter> letters_attack_;
|
||||
@@ -110,6 +111,10 @@ class TitleScene final : public Scene {
|
||||
static constexpr float DURATION_BLACK_SCREEN = 2.0F;
|
||||
static constexpr int MUSIC_FADE = 1500;
|
||||
|
||||
// Attract mode: temps d'inactivitat al títol (state MAIN) abans de saltar
|
||||
// a la demo. Qualsevol input el reseteja.
|
||||
static constexpr float TITLE_DEMO_TIMEOUT = 20.0F;
|
||||
|
||||
static constexpr float ORBIT_AMPLITUDE_X = 4.0F;
|
||||
static constexpr float ORBIT_AMPLITUDE_Y = 3.0F;
|
||||
static constexpr float ORBIT_FREQUENCY_X = 0.8F;
|
||||
|
||||
Reference in New Issue
Block a user