Files
spacetris/src/states/PlayingState.cpp

487 lines
18 KiB
C++

#include "PlayingState.h"
#include "../core/state/StateManager.h"
#include "../gameplay/core/Game.h"
#include "../gameplay/coop/CoopGame.h"
#include "../gameplay/effects/LineEffect.h"
#include "../persistence/Scores.h"
#include "../audio/Audio.h"
#include "../audio/SoundEffect.h"
#include "../graphics/renderers/GameRenderer.h"
#include "../core/Settings.h"
#include "../core/Config.h"
#include <SDL3/SDL.h>
// File-scope transport/spawn detection state
static uint64_t s_lastPieceSequence = 0;
static bool s_pendingTransport = false;
PlayingState::PlayingState(StateContext& ctx) : State(ctx) {}
void PlayingState::onEnter() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Entering Playing state");
// Initialize the game based on mode: endless/cooperate use chosen start level, challenge keeps its run state
if (ctx.game) {
if (ctx.game->getMode() == GameMode::Endless || ctx.game->getMode() == GameMode::Cooperate) {
if (ctx.startLevelSelection) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection);
ctx.game->reset(*ctx.startLevelSelection);
if (ctx.game->getMode() == GameMode::Cooperate && ctx.coopGame) {
ctx.coopGame->reset(*ctx.startLevelSelection);
}
}
} else {
// Challenge run is prepared before entering; ensure gameplay is unpaused
ctx.game->setPaused(false);
}
s_lastPieceSequence = ctx.game->getCurrentPieceSequence();
s_pendingTransport = false;
}
// (transport state is tracked at file scope)
}
void PlayingState::onExit() {
if (m_renderTarget) {
SDL_DestroyTexture(m_renderTarget);
m_renderTarget = nullptr;
}
}
void PlayingState::handleEvent(const SDL_Event& e) {
if (!ctx.game) return;
// If a transport animation is active, ignore gameplay input entirely.
if (GameRenderer::isTransportActive()) {
return;
}
const bool coopActive = ctx.game->getMode() == GameMode::Cooperate && ctx.coopGame;
auto setExitSelection = [&](int idx) {
if (ctx.exitPopupSelectedButton) {
*ctx.exitPopupSelectedButton = idx;
}
};
auto getExitSelection = [&]() -> int {
return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
};
if (e.type != SDL_EVENT_KEY_DOWN || e.key.repeat) {
return;
}
// If exit-confirm popup is visible, handle shortcuts here
if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) {
// Navigate between YES (0) and NO (1) buttons
if (e.key.scancode == SDL_SCANCODE_LEFT || e.key.scancode == SDL_SCANCODE_UP) {
setExitSelection(0);
return;
}
if (e.key.scancode == SDL_SCANCODE_RIGHT || e.key.scancode == SDL_SCANCODE_DOWN) {
setExitSelection(1);
return;
}
// Activate selected button with Enter or Space
if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) {
const bool confirmExit = (getExitSelection() == 0);
*ctx.showExitConfirmPopup = false;
if (confirmExit) {
// YES - Reset game and return to menu
if (ctx.startLevelSelection) {
ctx.game->reset(*ctx.startLevelSelection);
} else {
ctx.game->reset(0);
}
ctx.game->setPaused(false);
if (ctx.stateManager) ctx.stateManager->setState(AppState::Menu);
} else {
// NO - Just close popup and resume
ctx.game->setPaused(false);
}
return;
}
// Cancel with Esc (same as NO)
if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
*ctx.showExitConfirmPopup = false;
ctx.game->setPaused(false);
setExitSelection(1);
return;
}
// While modal is open, suppress other gameplay keys
return;
}
// ESC key - open confirmation popup
if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
if (ctx.showExitConfirmPopup) {
ctx.game->setPaused(true);
*ctx.showExitConfirmPopup = true;
setExitSelection(1); // Default to NO for safety
}
return;
}
// Debug: skip to next challenge level (B)
if (e.key.scancode == SDL_SCANCODE_B && ctx.game->getMode() == GameMode::Challenge) {
ctx.game->beginNextChallengeLevel();
// Cancel any countdown so play resumes immediately on the new level
if (ctx.gameplayCountdownActive) *ctx.gameplayCountdownActive = false;
if (ctx.menuPlayCountdownArmed) *ctx.menuPlayCountdownArmed = false;
ctx.game->setPaused(false);
return;
}
// Pause toggle (P) - matches classic behavior; disabled during countdown
if (e.key.scancode == SDL_SCANCODE_P) {
const bool countdown = (ctx.gameplayCountdownActive && *ctx.gameplayCountdownActive) ||
(ctx.menuPlayCountdownArmed && *ctx.menuPlayCountdownArmed);
if (!countdown) {
ctx.game->setPaused(!ctx.game->isPaused());
}
return;
}
// Tetris controls (only when not paused)
if (ctx.game->isPaused()) {
return;
}
if (coopActive && ctx.coopGame) {
// Player 1 (left): A/D move via DAS in ApplicationManager; here handle rotations/hold/hard-drop
if (e.key.scancode == SDL_SCANCODE_W) {
ctx.coopGame->rotate(CoopGame::PlayerSide::Left, 1);
return;
}
if (e.key.scancode == SDL_SCANCODE_Q) {
ctx.coopGame->rotate(CoopGame::PlayerSide::Left, -1);
return;
}
// Hard drop (left): keep LSHIFT, also allow E for convenience.
if (e.key.scancode == SDL_SCANCODE_LSHIFT || e.key.scancode == SDL_SCANCODE_E) {
SoundEffectManager::instance().playSound("hard_drop", 0.7f);
ctx.coopGame->hardDrop(CoopGame::PlayerSide::Left);
return;
}
if (e.key.scancode == SDL_SCANCODE_LCTRL) {
ctx.coopGame->holdCurrent(CoopGame::PlayerSide::Left);
return;
}
// Player 2 (right): arrow keys move via DAS; rotations/hold/hard-drop here
if (e.key.scancode == SDL_SCANCODE_UP) {
bool upIsCW = Settings::instance().isUpRotateClockwise();
ctx.coopGame->rotate(CoopGame::PlayerSide::Right, upIsCW ? 1 : -1);
return;
}
if (e.key.scancode == SDL_SCANCODE_RALT) {
ctx.coopGame->rotate(CoopGame::PlayerSide::Right, -1);
return;
}
// Hard drop (right): SPACE is the primary key for arrow controls; keep RSHIFT as an alternate.
if (e.key.scancode == SDL_SCANCODE_SPACE || e.key.scancode == SDL_SCANCODE_RSHIFT) {
SoundEffectManager::instance().playSound("hard_drop", 0.7f);
ctx.coopGame->hardDrop(CoopGame::PlayerSide::Right);
return;
}
if (e.key.scancode == SDL_SCANCODE_RCTRL) {
ctx.coopGame->holdCurrent(CoopGame::PlayerSide::Right);
return;
}
} else {
// Single-player classic controls
// Hold / swap current piece (H)
if (e.key.scancode == SDL_SCANCODE_H) {
ctx.game->holdCurrent();
return;
}
// Rotation (still event-based for precise timing)
if (e.key.scancode == SDL_SCANCODE_UP) {
// Use user setting to determine whether UP rotates clockwise
bool upIsCW = Settings::instance().isUpRotateClockwise();
ctx.game->rotate(upIsCW ? 1 : -1);
return;
}
if (e.key.scancode == SDL_SCANCODE_X) {
// Toggle the mapping so UP will rotate in the opposite direction
bool current = Settings::instance().isUpRotateClockwise();
Settings::instance().setUpRotateClockwise(!current);
Settings::instance().save();
// Play a subtle feedback sound if available
SoundEffectManager::instance().playSound("menu_toggle", 0.6f);
return;
}
// Hard drop (space)
if (e.key.scancode == SDL_SCANCODE_SPACE) {
SoundEffectManager::instance().playSound("hard_drop", 0.7f);
ctx.game->hardDrop();
return;
}
}
// Note: Left/Right movement and soft drop are now handled by
// ApplicationManager's update handler for proper DAS/ARR timing
}
void PlayingState::update(double frameMs) {
if (!ctx.game) return;
const bool coopActive = ctx.game->getMode() == GameMode::Cooperate && ctx.coopGame;
if (coopActive) {
// Visual effects only; gravity and movement handled from ApplicationManager for coop
ctx.coopGame->updateVisualEffects(frameMs);
// Update line clear effect for coop mode as well (renderer starts the effect)
if (ctx.lineEffect && ctx.lineEffect->isActive()) {
if (ctx.lineEffect->update(frameMs / 1000.0f)) {
ctx.coopGame->clearCompletedLines();
}
}
return;
}
ctx.game->updateVisualEffects(frameMs);
// If a transport animation is active, pause gameplay updates and ignore inputs
if (GameRenderer::isTransportActive()) {
// Keep visual effects updating but skip gravity/timers while transport runs
return;
}
// forward per-frame gameplay updates (gravity, line effects)
if (!ctx.game->isPaused()) {
ctx.game->tickGravity(frameMs);
// Detect spawn event (sequence increment) and request transport effect
uint64_t seq = ctx.game->getCurrentPieceSequence();
if (seq != s_lastPieceSequence) {
s_lastPieceSequence = seq;
s_pendingTransport = true;
}
ctx.game->updateElapsedTime();
if (ctx.lineEffect && ctx.lineEffect->isActive()) {
if (ctx.lineEffect->update(frameMs / 1000.0f)) {
ctx.game->clearCompletedLines();
}
}
}
// Note: Game over detection and state transition is now handled by ApplicationManager
}
void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
if (!ctx.game) return;
const bool coopActive = ctx.game->getMode() == GameMode::Cooperate && ctx.coopGame;
// Get current window size
int winW = 0, winH = 0;
SDL_GetRenderOutputSize(renderer, &winW, &winH);
// Create or resize render target if needed
if (!m_renderTarget || m_targetW != winW || m_targetH != winH) {
if (m_renderTarget) SDL_DestroyTexture(m_renderTarget);
m_renderTarget = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, winW, winH);
SDL_SetTextureBlendMode(m_renderTarget, SDL_BLENDMODE_BLEND);
m_targetW = winW;
m_targetH = winH;
}
bool paused = ctx.game->isPaused();
bool exitPopup = ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup;
bool countdown = (ctx.gameplayCountdownActive && *ctx.gameplayCountdownActive) ||
(ctx.menuPlayCountdownArmed && *ctx.menuPlayCountdownArmed);
bool challengeClearFx = ctx.challengeClearFxActive && *ctx.challengeClearFxActive;
const std::vector<int>* challengeClearOrder = ctx.challengeClearFxOrder;
double challengeClearElapsed = ctx.challengeClearFxElapsedMs ? *ctx.challengeClearFxElapsedMs : 0.0;
double challengeClearDuration = ctx.challengeClearFxDurationMs ? *ctx.challengeClearFxDurationMs : 0.0;
// Only blur if paused AND NOT in countdown (and not exit popup, though exit popup implies paused)
// Actually, exit popup should probably still blur/dim.
// But countdown should definitely NOT show the "PAUSED" overlay.
bool shouldBlur = paused && !countdown && !challengeClearFx;
if (shouldBlur && m_renderTarget) {
// Render game to texture
SDL_SetRenderTarget(renderer, m_renderTarget);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
// Apply the same view/scale as main.cpp uses
SDL_SetRenderViewport(renderer, &logicalVP);
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
// Render game content (no overlays)
// If a transport effect was requested due to a recent spawn, start it here so
// the renderer has the correct layout and renderer context to compute coords.
if (!coopActive && s_pendingTransport) {
GameRenderer::startTransportEffectForGame(ctx.game, ctx.blocksTex, 1200.0f, 1000.0f, logicalScale, (float)winW, (float)winH, 0.4f);
s_pendingTransport = false;
}
if (coopActive && ctx.coopGame) {
GameRenderer::renderCoopPlayingState(
renderer,
ctx.coopGame,
ctx.pixelFont,
ctx.lineEffect,
ctx.blocksTex,
ctx.statisticsPanelTex,
ctx.scorePanelTex,
ctx.nextPanelTex,
ctx.holdPanelTex,
paused,
1200.0f,
1000.0f,
logicalScale,
(float)winW,
(float)winH
);
} else {
GameRenderer::renderPlayingState(
renderer,
ctx.game,
ctx.pixelFont,
ctx.lineEffect,
ctx.blocksTex,
ctx.asteroidsTex,
ctx.statisticsPanelTex,
ctx.scorePanelTex,
ctx.nextPanelTex,
ctx.holdPanelTex,
countdown,
1200.0f, // LOGICAL_W
1000.0f, // LOGICAL_H
logicalScale,
(float)winW,
(float)winH,
challengeClearFx,
challengeClearOrder,
challengeClearElapsed,
challengeClearDuration,
countdown ? nullptr : ctx.challengeStoryText,
countdown ? 0.0f : (ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f)
);
}
// Reset to screen
SDL_SetRenderTarget(renderer, nullptr);
// Draw blurred texture
SDL_Rect oldVP;
SDL_GetRenderViewport(renderer, &oldVP);
float oldSX, oldSY;
SDL_GetRenderScale(renderer, &oldSX, &oldSY);
SDL_SetRenderViewport(renderer, nullptr);
SDL_SetRenderScale(renderer, 1.0f, 1.0f);
SDL_FRect dst{0, 0, (float)winW, (float)winH};
// Blur pass (accumulate multiple offset copies)
int offset = Config::Visuals::PAUSE_BLUR_OFFSET;
int iterations = Config::Visuals::PAUSE_BLUR_ITERATIONS;
// Base layer
SDL_SetTextureAlphaMod(m_renderTarget, Config::Visuals::PAUSE_BLUR_ALPHA);
SDL_RenderTexture(renderer, m_renderTarget, nullptr, &dst);
// Accumulate offset layers
for (int i = 1; i <= iterations; ++i) {
float currentOffset = (float)(offset * i);
SDL_FRect d1 = dst; d1.x -= currentOffset; d1.y -= currentOffset;
SDL_RenderTexture(renderer, m_renderTarget, nullptr, &d1);
SDL_FRect d2 = dst; d2.x += currentOffset; d2.y -= currentOffset;
SDL_RenderTexture(renderer, m_renderTarget, nullptr, &d2);
SDL_FRect d3 = dst; d3.x -= currentOffset; d3.y += currentOffset;
SDL_RenderTexture(renderer, m_renderTarget, nullptr, &d3);
SDL_FRect d4 = dst; d4.x += currentOffset; d4.y += currentOffset;
SDL_RenderTexture(renderer, m_renderTarget, nullptr, &d4);
}
SDL_SetTextureAlphaMod(m_renderTarget, 255);
// Restore state
SDL_SetRenderViewport(renderer, &oldVP);
SDL_SetRenderScale(renderer, oldSX, oldSY);
// Draw overlays
if (exitPopup) {
GameRenderer::renderExitPopup(
renderer,
ctx.pixelFont,
(float)winW,
(float)winH,
logicalScale,
(ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1)
);
} else {
GameRenderer::renderPauseOverlay(
renderer,
ctx.pixelFont,
(float)winW,
(float)winH,
logicalScale
);
}
} else {
// Render normally directly to screen
if (!coopActive && s_pendingTransport) {
GameRenderer::startTransportEffectForGame(ctx.game, ctx.blocksTex, 1200.0f, 1000.0f, logicalScale, (float)winW, (float)winH, 0.4f);
s_pendingTransport = false;
}
if (coopActive && ctx.coopGame) {
GameRenderer::renderCoopPlayingState(
renderer,
ctx.coopGame,
ctx.pixelFont,
ctx.lineEffect,
ctx.blocksTex,
ctx.statisticsPanelTex,
ctx.scorePanelTex,
ctx.nextPanelTex,
ctx.holdPanelTex,
paused,
1200.0f,
1000.0f,
logicalScale,
(float)winW,
(float)winH
);
} else {
GameRenderer::renderPlayingState(
renderer,
ctx.game,
ctx.pixelFont,
ctx.lineEffect,
ctx.blocksTex,
ctx.asteroidsTex,
ctx.statisticsPanelTex,
ctx.scorePanelTex,
ctx.nextPanelTex,
ctx.holdPanelTex,
countdown,
1200.0f,
1000.0f,
logicalScale,
(float)winW,
(float)winH,
challengeClearFx,
challengeClearOrder,
challengeClearElapsed,
challengeClearDuration,
countdown ? nullptr : ctx.challengeStoryText,
countdown ? 0.0f : (ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f)
);
}
}
}