fixed gameplay
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 196 KiB |
BIN
assets/music/asteroid-destroy.mp3
Normal file
BIN
assets/music/asteroid-destroy.mp3
Normal file
Binary file not shown.
@ -6,7 +6,7 @@ Fullscreen=1
|
|||||||
|
|
||||||
[Audio]
|
[Audio]
|
||||||
Music=1
|
Music=1
|
||||||
Sound=0
|
Sound=1
|
||||||
|
|
||||||
[Gameplay]
|
[Gameplay]
|
||||||
SmoothScroll=1
|
SmoothScroll=1
|
||||||
|
|||||||
@ -200,6 +200,14 @@ struct TetrisApp::Impl {
|
|||||||
bool countdownAdvancesChallenge = false;
|
bool countdownAdvancesChallenge = false;
|
||||||
double gameplayBackgroundClockMs = 0.0;
|
double gameplayBackgroundClockMs = 0.0;
|
||||||
|
|
||||||
|
// Challenge clear FX (celebratory board explosion before countdown)
|
||||||
|
bool challengeClearFxActive = false;
|
||||||
|
double challengeClearFxElapsedMs = 0.0;
|
||||||
|
double challengeClearFxDurationMs = 0.0;
|
||||||
|
int challengeClearFxNextLevel = 0;
|
||||||
|
std::vector<int> challengeClearFxOrder;
|
||||||
|
std::mt19937 challengeClearFxRng{std::random_device{}()};
|
||||||
|
|
||||||
std::unique_ptr<StateManager> stateMgr;
|
std::unique_ptr<StateManager> stateMgr;
|
||||||
StateContext ctx{};
|
StateContext ctx{};
|
||||||
std::unique_ptr<LoadingState> loadingState;
|
std::unique_ptr<LoadingState> loadingState;
|
||||||
@ -386,6 +394,10 @@ int TetrisApp::Impl::init()
|
|||||||
suppressLineVoiceForLevelUp = true;
|
suppressLineVoiceForLevelUp = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
game->setAsteroidDestroyedCallback([](AsteroidType /*type*/) {
|
||||||
|
SoundEffectManager::instance().playSound("asteroid_destroy", 0.9f);
|
||||||
|
});
|
||||||
|
|
||||||
state = AppState::Loading;
|
state = AppState::Loading;
|
||||||
loadingProgress = 0.0;
|
loadingProgress = 0.0;
|
||||||
loadStart = SDL_GetTicks();
|
loadStart = SDL_GetTicks();
|
||||||
@ -448,6 +460,10 @@ int TetrisApp::Impl::init()
|
|||||||
ctx.exitPopupSelectedButton = &exitPopupSelectedButton;
|
ctx.exitPopupSelectedButton = &exitPopupSelectedButton;
|
||||||
ctx.gameplayCountdownActive = &gameplayCountdownActive;
|
ctx.gameplayCountdownActive = &gameplayCountdownActive;
|
||||||
ctx.menuPlayCountdownArmed = &menuPlayCountdownArmed;
|
ctx.menuPlayCountdownArmed = &menuPlayCountdownArmed;
|
||||||
|
ctx.challengeClearFxActive = &challengeClearFxActive;
|
||||||
|
ctx.challengeClearFxElapsedMs = &challengeClearFxElapsedMs;
|
||||||
|
ctx.challengeClearFxDurationMs = &challengeClearFxDurationMs;
|
||||||
|
ctx.challengeClearFxOrder = &challengeClearFxOrder;
|
||||||
ctx.playerName = &playerName;
|
ctx.playerName = &playerName;
|
||||||
ctx.fullscreenFlag = &isFullscreen;
|
ctx.fullscreenFlag = &isFullscreen;
|
||||||
ctx.applyFullscreen = [this](bool enable) {
|
ctx.applyFullscreen = [this](bool enable) {
|
||||||
@ -568,6 +584,37 @@ void TetrisApp::Impl::runLoop()
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto startChallengeClearFx = [this](int nextLevel) {
|
||||||
|
challengeClearFxOrder.clear();
|
||||||
|
const auto& boardRef = game->boardRef();
|
||||||
|
const auto& asteroidRef = game->asteroidCells();
|
||||||
|
for (int idx = 0; idx < Game::COLS * Game::ROWS; ++idx) {
|
||||||
|
if (boardRef[idx] != 0 || asteroidRef[idx].has_value()) {
|
||||||
|
challengeClearFxOrder.push_back(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (challengeClearFxOrder.empty()) {
|
||||||
|
challengeClearFxOrder.reserve(Game::COLS * Game::ROWS);
|
||||||
|
for (int idx = 0; idx < Game::COLS * Game::ROWS; ++idx) {
|
||||||
|
challengeClearFxOrder.push_back(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::shuffle(challengeClearFxOrder.begin(), challengeClearFxOrder.end(), challengeClearFxRng);
|
||||||
|
|
||||||
|
challengeClearFxElapsedMs = 0.0;
|
||||||
|
challengeClearFxDurationMs = std::clamp(800.0 + static_cast<double>(challengeClearFxOrder.size()) * 8.0, 900.0, 2600.0);
|
||||||
|
challengeClearFxNextLevel = nextLevel;
|
||||||
|
challengeClearFxActive = true;
|
||||||
|
gameplayCountdownActive = false;
|
||||||
|
gameplayCountdownElapsed = 0.0;
|
||||||
|
gameplayCountdownIndex = 0;
|
||||||
|
menuPlayCountdownArmed = false;
|
||||||
|
if (game) {
|
||||||
|
game->setPaused(true);
|
||||||
|
}
|
||||||
|
SoundEffectManager::instance().playSound("challenge_clear", 0.8f);
|
||||||
|
};
|
||||||
|
|
||||||
while (running)
|
while (running)
|
||||||
{
|
{
|
||||||
if (!ctx.scores && scoresLoadComplete.load(std::memory_order_acquire)) {
|
if (!ctx.scores && scoresLoadComplete.load(std::memory_order_acquire)) {
|
||||||
@ -756,6 +803,8 @@ void TetrisApp::Impl::runLoop()
|
|||||||
case ui::BottomMenuItem::Challenge:
|
case ui::BottomMenuItem::Challenge:
|
||||||
if (game) {
|
if (game) {
|
||||||
game->setMode(GameMode::Challenge);
|
game->setMode(GameMode::Challenge);
|
||||||
|
// Suppress the initial level-up jingle when starting Challenge from menu
|
||||||
|
skipNextLevelUpJingle = true;
|
||||||
game->startChallengeRun(1);
|
game->startChallengeRun(1);
|
||||||
}
|
}
|
||||||
startMenuPlayTransition();
|
startMenuPlayTransition();
|
||||||
@ -876,6 +925,32 @@ void TetrisApp::Impl::runLoop()
|
|||||||
if (frameMs > 100.0) frameMs = 100.0;
|
if (frameMs > 100.0) frameMs = 100.0;
|
||||||
gameplayBackgroundClockMs += frameMs;
|
gameplayBackgroundClockMs += frameMs;
|
||||||
|
|
||||||
|
if (challengeClearFxActive) {
|
||||||
|
challengeClearFxElapsedMs += frameMs;
|
||||||
|
if (challengeClearFxElapsedMs >= challengeClearFxDurationMs) {
|
||||||
|
challengeClearFxElapsedMs = challengeClearFxDurationMs;
|
||||||
|
challengeClearFxActive = false;
|
||||||
|
if (challengeClearFxNextLevel > 0) {
|
||||||
|
// Advance to the next challenge level immediately so the countdown shows the new board/asteroids
|
||||||
|
if (game) {
|
||||||
|
game->beginNextChallengeLevel();
|
||||||
|
game->setPaused(true);
|
||||||
|
}
|
||||||
|
gameplayCountdownSource = CountdownSource::ChallengeLevel;
|
||||||
|
countdownLevel = challengeClearFxNextLevel;
|
||||||
|
countdownGoalAsteroids = challengeClearFxNextLevel;
|
||||||
|
countdownAdvancesChallenge = false; // already advanced
|
||||||
|
gameplayCountdownActive = true;
|
||||||
|
menuPlayCountdownArmed = false;
|
||||||
|
gameplayCountdownElapsed = 0.0;
|
||||||
|
gameplayCountdownIndex = 0;
|
||||||
|
SoundEffectManager::instance().playSound("new_level", 1.0f);
|
||||||
|
skipNextLevelUpJingle = true;
|
||||||
|
}
|
||||||
|
challengeClearFxNextLevel = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const bool *ks = SDL_GetKeyboardState(nullptr);
|
const bool *ks = SDL_GetKeyboardState(nullptr);
|
||||||
bool left = state == AppState::Playing && ks[SDL_SCANCODE_LEFT];
|
bool left = state == AppState::Playing && ks[SDL_SCANCODE_LEFT];
|
||||||
bool right = state == AppState::Playing && ks[SDL_SCANCODE_RIGHT];
|
bool right = state == AppState::Playing && ks[SDL_SCANCODE_RIGHT];
|
||||||
@ -1013,9 +1088,9 @@ void TetrisApp::Impl::runLoop()
|
|||||||
SoundEffectManager::instance().init();
|
SoundEffectManager::instance().init();
|
||||||
loadedTasks.fetch_add(1);
|
loadedTasks.fetch_add(1);
|
||||||
|
|
||||||
const std::vector<std::string> audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level"};
|
const std::vector<std::string> audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level","asteroid_destroy","challenge_clear"};
|
||||||
for (const auto &id : audioIds) {
|
for (const auto &id : audioIds) {
|
||||||
std::string basePath = "assets/music/" + (id == "hard_drop" ? "hard_drop_001" : id);
|
std::string basePath = "assets/music/" + (id == "hard_drop" ? "hard_drop_001" : (id == "challenge_clear" ? "GONG0" : id));
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lk(currentLoadingMutex);
|
std::lock_guard<std::mutex> lk(currentLoadingMutex);
|
||||||
currentLoadingFile = basePath;
|
currentLoadingFile = basePath;
|
||||||
@ -1224,20 +1299,10 @@ void TetrisApp::Impl::runLoop()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == AppState::Playing && game && game->getMode() == GameMode::Challenge && !gameplayCountdownActive) {
|
if (state == AppState::Playing && game && game->getMode() == GameMode::Challenge && !gameplayCountdownActive && !challengeClearFxActive) {
|
||||||
int queuedLevel = game->consumeQueuedChallengeLevel();
|
int queuedLevel = game->consumeQueuedChallengeLevel();
|
||||||
if (queuedLevel > 0) {
|
if (queuedLevel > 0) {
|
||||||
gameplayCountdownSource = CountdownSource::ChallengeLevel;
|
startChallengeClearFx(queuedLevel);
|
||||||
countdownLevel = queuedLevel;
|
|
||||||
countdownGoalAsteroids = queuedLevel;
|
|
||||||
countdownAdvancesChallenge = true;
|
|
||||||
gameplayCountdownActive = true;
|
|
||||||
menuPlayCountdownArmed = false;
|
|
||||||
gameplayCountdownElapsed = 0.0;
|
|
||||||
gameplayCountdownIndex = 0;
|
|
||||||
game->setPaused(true);
|
|
||||||
SoundEffectManager::instance().playSound("new_level", 1.0f);
|
|
||||||
skipNextLevelUpJingle = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1339,6 +1404,14 @@ void TetrisApp::Impl::runLoop()
|
|||||||
game->setPaused(false);
|
game->setPaused(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state != AppState::Playing && challengeClearFxActive) {
|
||||||
|
challengeClearFxActive = false;
|
||||||
|
challengeClearFxElapsedMs = 0.0;
|
||||||
|
challengeClearFxDurationMs = 0.0;
|
||||||
|
challengeClearFxNextLevel = 0;
|
||||||
|
challengeClearFxOrder.clear();
|
||||||
|
}
|
||||||
|
|
||||||
SDL_SetRenderViewport(renderer, nullptr);
|
SDL_SetRenderViewport(renderer, nullptr);
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
||||||
SDL_RenderClear(renderer);
|
SDL_RenderClear(renderer);
|
||||||
|
|||||||
@ -567,6 +567,7 @@ void Game::handleAsteroidsOnClearedRows(const std::vector<int>& clearedRows,
|
|||||||
|
|
||||||
// Track asteroid count updates during processing
|
// Track asteroid count updates during processing
|
||||||
int destroyedThisPass = 0;
|
int destroyedThisPass = 0;
|
||||||
|
std::optional<AsteroidType> lastDestroyedType;
|
||||||
|
|
||||||
// Precompute how many cleared rows are at or below each row to reposition survivors
|
// Precompute how many cleared rows are at or below each row to reposition survivors
|
||||||
std::array<int, ROWS> clearedBelow{};
|
std::array<int, ROWS> clearedBelow{};
|
||||||
@ -595,6 +596,7 @@ void Game::handleAsteroidsOnClearedRows(const std::vector<int>& clearedRows,
|
|||||||
}
|
}
|
||||||
if (cell.hitsRemaining == 0) {
|
if (cell.hitsRemaining == 0) {
|
||||||
destroyedThisPass++;
|
destroyedThisPass++;
|
||||||
|
lastDestroyedType = cell.type;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -627,6 +629,9 @@ void Game::handleAsteroidsOnClearedRows(const std::vector<int>& clearedRows,
|
|||||||
|
|
||||||
if (destroyedThisPass > 0) {
|
if (destroyedThisPass > 0) {
|
||||||
asteroidsRemainingCount = std::max(0, asteroidsRemainingCount - destroyedThisPass);
|
asteroidsRemainingCount = std::max(0, asteroidsRemainingCount - destroyedThisPass);
|
||||||
|
if (asteroidDestroyedCallback && lastDestroyedType.has_value()) {
|
||||||
|
asteroidDestroyedCallback(*lastDestroyedType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -88,8 +88,10 @@ public:
|
|||||||
// Sound effect callbacks
|
// Sound effect callbacks
|
||||||
using SoundCallback = std::function<void(int)>; // Callback for line clear sounds (number of lines)
|
using SoundCallback = std::function<void(int)>; // Callback for line clear sounds (number of lines)
|
||||||
using LevelUpCallback = std::function<void(int)>; // Callback for level up sounds
|
using LevelUpCallback = std::function<void(int)>; // Callback for level up sounds
|
||||||
|
using AsteroidDestroyedCallback = std::function<void(AsteroidType)>; // Callback when an asteroid is fully destroyed
|
||||||
void setSoundCallback(SoundCallback callback) { soundCallback = callback; }
|
void setSoundCallback(SoundCallback callback) { soundCallback = callback; }
|
||||||
void setLevelUpCallback(LevelUpCallback callback) { levelUpCallback = callback; }
|
void setLevelUpCallback(LevelUpCallback callback) { levelUpCallback = callback; }
|
||||||
|
void setAsteroidDestroyedCallback(AsteroidDestroyedCallback callback) { asteroidDestroyedCallback = callback; }
|
||||||
|
|
||||||
// Shape helper --------------------------------------------------------
|
// Shape helper --------------------------------------------------------
|
||||||
static bool cellFilled(const Piece& p, int cx, int cy);
|
static bool cellFilled(const Piece& p, int cx, int cy);
|
||||||
@ -147,6 +149,7 @@ private:
|
|||||||
// Sound effect callbacks
|
// Sound effect callbacks
|
||||||
SoundCallback soundCallback;
|
SoundCallback soundCallback;
|
||||||
LevelUpCallback levelUpCallback;
|
LevelUpCallback levelUpCallback;
|
||||||
|
AsteroidDestroyedCallback asteroidDestroyedCallback;
|
||||||
// Gravity tuning -----------------------------------------------------
|
// Gravity tuning -----------------------------------------------------
|
||||||
// Global multiplier applied to all level timings (use to slow/speed whole-game gravity)
|
// Global multiplier applied to all level timings (use to slow/speed whole-game gravity)
|
||||||
double gravityGlobalMultiplier{1.0};
|
double gravityGlobalMultiplier{1.0};
|
||||||
|
|||||||
@ -581,7 +581,11 @@ void GameRenderer::renderPlayingState(
|
|||||||
float logicalH,
|
float logicalH,
|
||||||
float logicalScale,
|
float logicalScale,
|
||||||
float winW,
|
float winW,
|
||||||
float winH
|
float winH,
|
||||||
|
bool challengeClearFxActive,
|
||||||
|
const std::vector<int>* challengeClearFxOrder,
|
||||||
|
double challengeClearFxElapsedMs,
|
||||||
|
double challengeClearFxDurationMs
|
||||||
) {
|
) {
|
||||||
if (!game || !pixelFont) return;
|
if (!game || !pixelFont) return;
|
||||||
|
|
||||||
@ -997,6 +1001,25 @@ void GameRenderer::renderPlayingState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::array<float, Game::COLS * Game::ROWS> challengeClearMask{};
|
||||||
|
const bool challengeClearActive = challengeClearFxActive && challengeClearFxOrder && !challengeClearFxOrder->empty() && challengeClearFxDurationMs > 0.0;
|
||||||
|
if (challengeClearActive) {
|
||||||
|
const double totalDuration = std::max(50.0, challengeClearFxDurationMs);
|
||||||
|
const double perCell = totalDuration / static_cast<double>(challengeClearFxOrder->size());
|
||||||
|
for (size_t i = 0; i < challengeClearFxOrder->size(); ++i) {
|
||||||
|
int idx = (*challengeClearFxOrder)[i];
|
||||||
|
if (idx < 0 || idx >= static_cast<int>(challengeClearMask.size())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double startMs = perCell * static_cast<double>(i);
|
||||||
|
double local = (challengeClearFxElapsedMs - startMs) / perCell;
|
||||||
|
float progress = static_cast<float>(std::clamp(local, 0.0, 1.0));
|
||||||
|
if (progress > 0.0f) {
|
||||||
|
challengeClearMask[idx] = progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int y = 0; y < Game::ROWS; ++y) {
|
for (int y = 0; y < Game::ROWS; ++y) {
|
||||||
float dropOffset = rowDropOffsets[y];
|
float dropOffset = rowDropOffsets[y];
|
||||||
for (int x = 0; x < Game::COLS; ++x) {
|
for (int x = 0; x < Game::COLS; ++x) {
|
||||||
@ -1015,6 +1038,21 @@ void GameRenderer::renderPlayingState(
|
|||||||
by += amplitude * 0.75f * std::cos(t * (freq + 1.1f));
|
by += amplitude * 0.75f * std::cos(t * (freq + 1.1f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float clearProgress = challengeClearMask[cellIdx];
|
||||||
|
float clearAlpha = 1.0f;
|
||||||
|
float clearScale = 1.0f;
|
||||||
|
if (clearProgress > 0.0f) {
|
||||||
|
float eased = smoothstep(clearProgress);
|
||||||
|
clearAlpha = std::max(0.0f, 1.0f - eased);
|
||||||
|
clearScale = 1.0f + 0.35f * eased;
|
||||||
|
float offset = (finalBlockSize - finalBlockSize * clearScale) * 0.5f;
|
||||||
|
bx += offset;
|
||||||
|
by += offset;
|
||||||
|
float jitter = eased * 2.0f;
|
||||||
|
bx += std::sin(static_cast<float>(cellIdx) * 3.1f) * jitter;
|
||||||
|
by += std::cos(static_cast<float>(cellIdx) * 2.3f) * jitter * 0.6f;
|
||||||
|
}
|
||||||
|
|
||||||
bool isAsteroid = challengeMode && asteroidCells[cellIdx].has_value();
|
bool isAsteroid = challengeMode && asteroidCells[cellIdx].has_value();
|
||||||
if (isAsteroid) {
|
if (isAsteroid) {
|
||||||
const AsteroidCell& cell = *asteroidCells[cellIdx];
|
const AsteroidCell& cell = *asteroidCells[cellIdx];
|
||||||
@ -1034,15 +1072,25 @@ void GameRenderer::renderPlayingState(
|
|||||||
SDL_SetTextureAlphaMod(asteroidsTex, static_cast<Uint8>(std::clamp(spawnAlpha, 0.0f, 1.0f) * 255.0f));
|
SDL_SetTextureAlphaMod(asteroidsTex, static_cast<Uint8>(std::clamp(spawnAlpha, 0.0f, 1.0f) * 255.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
float size = finalBlockSize * spawnScale;
|
float size = finalBlockSize * spawnScale * clearScale;
|
||||||
float offset = (finalBlockSize - size) * 0.5f;
|
float offset = (finalBlockSize - size) * 0.5f;
|
||||||
|
if (asteroidsTex && clearAlpha < 1.0f) {
|
||||||
|
Uint8 alpha = static_cast<Uint8>(std::clamp(spawnAlpha * clearAlpha, 0.0f, 1.0f) * 255.0f);
|
||||||
|
SDL_SetTextureAlphaMod(asteroidsTex, alpha);
|
||||||
|
}
|
||||||
drawAsteroid(renderer, asteroidsTex, bx + offset, by + offset, size, cell);
|
drawAsteroid(renderer, asteroidsTex, bx + offset, by + offset, size, cell);
|
||||||
|
|
||||||
if (asteroidsTex && spawnAlpha < 1.0f) {
|
if (asteroidsTex && (spawnAlpha < 1.0f || clearAlpha < 1.0f)) {
|
||||||
SDL_SetTextureAlphaMod(asteroidsTex, 255);
|
SDL_SetTextureAlphaMod(asteroidsTex, 255);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1);
|
if (blocksTex && clearAlpha < 1.0f) {
|
||||||
|
SDL_SetTextureAlphaMod(blocksTex, static_cast<Uint8>(std::clamp(clearAlpha, 0.0f, 1.0f) * 255.0f));
|
||||||
|
}
|
||||||
|
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize * clearScale, v - 1);
|
||||||
|
if (blocksTex && clearAlpha < 1.0f) {
|
||||||
|
SDL_SetTextureAlphaMod(blocksTex, 255);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1075,7 +1123,7 @@ void GameRenderer::renderPlayingState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool allowActivePieceRender = !GameRenderer::isTransportActive();
|
bool allowActivePieceRender = !GameRenderer::isTransportActive() && !challengeClearActive;
|
||||||
const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled();
|
const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled();
|
||||||
|
|
||||||
float activePiecePixelOffsetX = 0.0f;
|
float activePiecePixelOffsetX = 0.0f;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
#include <vector>
|
||||||
#include "../../gameplay/core/Game.h"
|
#include "../../gameplay/core/Game.h"
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
@ -31,7 +32,11 @@ public:
|
|||||||
float logicalH,
|
float logicalH,
|
||||||
float logicalScale,
|
float logicalScale,
|
||||||
float winW,
|
float winW,
|
||||||
float winH
|
float winH,
|
||||||
|
bool challengeClearFxActive = false,
|
||||||
|
const std::vector<int>* challengeClearFxOrder = nullptr,
|
||||||
|
double challengeClearFxElapsedMs = 0.0,
|
||||||
|
double challengeClearFxDurationMs = 0.0
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render the pause overlay (full screen)
|
// Render the pause overlay (full screen)
|
||||||
|
|||||||
@ -221,11 +221,15 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
|||||||
bool exitPopup = ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup;
|
bool exitPopup = ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup;
|
||||||
bool countdown = (ctx.gameplayCountdownActive && *ctx.gameplayCountdownActive) ||
|
bool countdown = (ctx.gameplayCountdownActive && *ctx.gameplayCountdownActive) ||
|
||||||
(ctx.menuPlayCountdownArmed && *ctx.menuPlayCountdownArmed);
|
(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)
|
// 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.
|
// Actually, exit popup should probably still blur/dim.
|
||||||
// But countdown should definitely NOT show the "PAUSED" overlay.
|
// But countdown should definitely NOT show the "PAUSED" overlay.
|
||||||
bool shouldBlur = paused && !countdown;
|
bool shouldBlur = paused && !countdown && !challengeClearFx;
|
||||||
|
|
||||||
if (shouldBlur && m_renderTarget) {
|
if (shouldBlur && m_renderTarget) {
|
||||||
// Render game to texture
|
// Render game to texture
|
||||||
@ -261,7 +265,11 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
|||||||
1000.0f, // LOGICAL_H
|
1000.0f, // LOGICAL_H
|
||||||
logicalScale,
|
logicalScale,
|
||||||
(float)winW,
|
(float)winW,
|
||||||
(float)winH
|
(float)winH,
|
||||||
|
challengeClearFx,
|
||||||
|
challengeClearOrder,
|
||||||
|
challengeClearElapsed,
|
||||||
|
challengeClearDuration
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reset to screen
|
// Reset to screen
|
||||||
@ -351,7 +359,11 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
|||||||
1000.0f,
|
1000.0f,
|
||||||
logicalScale,
|
logicalScale,
|
||||||
(float)winW,
|
(float)winW,
|
||||||
(float)winH
|
(float)winH,
|
||||||
|
challengeClearFx,
|
||||||
|
challengeClearOrder,
|
||||||
|
challengeClearElapsed,
|
||||||
|
challengeClearDuration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,6 +67,11 @@ struct StateContext {
|
|||||||
int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default)
|
int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default)
|
||||||
bool* gameplayCountdownActive = nullptr; // True if start-of-game countdown is running
|
bool* gameplayCountdownActive = nullptr; // True if start-of-game countdown is running
|
||||||
bool* menuPlayCountdownArmed = nullptr; // True if we are transitioning to play and countdown is pending
|
bool* menuPlayCountdownArmed = nullptr; // True if we are transitioning to play and countdown is pending
|
||||||
|
// Challenge clear FX (slow block-by-block explosion before next level)
|
||||||
|
bool* challengeClearFxActive = nullptr;
|
||||||
|
double* challengeClearFxElapsedMs = nullptr;
|
||||||
|
double* challengeClearFxDurationMs = nullptr;
|
||||||
|
std::vector<int>* challengeClearFxOrder = nullptr;
|
||||||
std::string* playerName = nullptr; // Shared player name buffer for highscores/options
|
std::string* playerName = nullptr; // Shared player name buffer for highscores/options
|
||||||
bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available
|
bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available
|
||||||
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes
|
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes
|
||||||
|
|||||||
Reference in New Issue
Block a user