374 lines
14 KiB
C++
374 lines
14 KiB
C++
#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 <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 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<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 (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,
|
|
ctx.challengeStoryText,
|
|
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,
|
|
ctx.challengeStoryText,
|
|
ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f
|
|
);
|
|
}
|
|
}
|