#include "PlayingState.h" #include "../core/state/StateManager.h" #include "../gameplay/core/Game.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 // 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 uses chosen start level, challenge keeps its run state if (ctx.game) { if (ctx.game->getMode() == GameMode::Endless) { if (ctx.startLevelSelection) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection); ctx.game->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 a transport animation is active, ignore gameplay input entirely. if (GameRenderer::isTransportActive()) { return; } // We keep short-circuited input here; main still owns mouse UI if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { if (!ctx.game) return; auto setExitSelection = [&](int value) { if (ctx.exitPopupSelectedButton) { *ctx.exitPopupSelectedButton = value; } }; auto getExitSelection = [&]() -> int { return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1; }; // Pause toggle (P) if (e.key.scancode == SDL_SCANCODE_P) { bool paused = ctx.game->isPaused(); ctx.game->setPaused(!paused); 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) { if (ctx.game) 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 && 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; } // Tetris controls (only when not paused) if (!ctx.game->isPaused()) { // 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; 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; // 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* 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 (s_pendingTransport) { GameRenderer::startTransportEffectForGame(ctx.game, ctx.blocksTex, 1200.0f, 1000.0f, logicalScale, (float)winW, (float)winH, 0.4f); s_pendingTransport = false; } 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 (s_pendingTransport) { GameRenderer::startTransportEffectForGame(ctx.game, ctx.blocksTex, 1200.0f, 1000.0f, logicalScale, (float)winW, (float)winH, 0.4f); s_pendingTransport = false; } 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) ); } }