fixed gameplay

This commit is contained in:
2025-12-20 15:17:35 +01:00
parent 9a3c1a0688
commit ad014e1de0
10 changed files with 176 additions and 25 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

View File

@ -6,7 +6,7 @@ Fullscreen=1
[Audio]
Music=1
Sound=0
Sound=1
[Gameplay]
SmoothScroll=1

View File

@ -200,6 +200,14 @@ struct TetrisApp::Impl {
bool countdownAdvancesChallenge = false;
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;
StateContext ctx{};
std::unique_ptr<LoadingState> loadingState;
@ -386,6 +394,10 @@ int TetrisApp::Impl::init()
suppressLineVoiceForLevelUp = true;
});
game->setAsteroidDestroyedCallback([](AsteroidType /*type*/) {
SoundEffectManager::instance().playSound("asteroid_destroy", 0.9f);
});
state = AppState::Loading;
loadingProgress = 0.0;
loadStart = SDL_GetTicks();
@ -448,6 +460,10 @@ int TetrisApp::Impl::init()
ctx.exitPopupSelectedButton = &exitPopupSelectedButton;
ctx.gameplayCountdownActive = &gameplayCountdownActive;
ctx.menuPlayCountdownArmed = &menuPlayCountdownArmed;
ctx.challengeClearFxActive = &challengeClearFxActive;
ctx.challengeClearFxElapsedMs = &challengeClearFxElapsedMs;
ctx.challengeClearFxDurationMs = &challengeClearFxDurationMs;
ctx.challengeClearFxOrder = &challengeClearFxOrder;
ctx.playerName = &playerName;
ctx.fullscreenFlag = &isFullscreen;
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)
{
if (!ctx.scores && scoresLoadComplete.load(std::memory_order_acquire)) {
@ -756,6 +803,8 @@ void TetrisApp::Impl::runLoop()
case ui::BottomMenuItem::Challenge:
if (game) {
game->setMode(GameMode::Challenge);
// Suppress the initial level-up jingle when starting Challenge from menu
skipNextLevelUpJingle = true;
game->startChallengeRun(1);
}
startMenuPlayTransition();
@ -876,6 +925,32 @@ void TetrisApp::Impl::runLoop()
if (frameMs > 100.0) frameMs = 100.0;
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);
bool left = state == AppState::Playing && ks[SDL_SCANCODE_LEFT];
bool right = state == AppState::Playing && ks[SDL_SCANCODE_RIGHT];
@ -1013,9 +1088,9 @@ void TetrisApp::Impl::runLoop()
SoundEffectManager::instance().init();
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) {
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);
currentLoadingFile = basePath;
@ -1224,20 +1299,10 @@ void TetrisApp::Impl::runLoop()
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();
if (queuedLevel > 0) {
gameplayCountdownSource = CountdownSource::ChallengeLevel;
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;
startChallengeClearFx(queuedLevel);
}
}
@ -1339,6 +1404,14 @@ void TetrisApp::Impl::runLoop()
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_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);

View File

@ -567,6 +567,7 @@ void Game::handleAsteroidsOnClearedRows(const std::vector<int>& clearedRows,
// Track asteroid count updates during processing
int destroyedThisPass = 0;
std::optional<AsteroidType> lastDestroyedType;
// Precompute how many cleared rows are at or below each row to reposition survivors
std::array<int, ROWS> clearedBelow{};
@ -595,6 +596,7 @@ void Game::handleAsteroidsOnClearedRows(const std::vector<int>& clearedRows,
}
if (cell.hitsRemaining == 0) {
destroyedThisPass++;
lastDestroyedType = cell.type;
continue;
}
@ -627,6 +629,9 @@ void Game::handleAsteroidsOnClearedRows(const std::vector<int>& clearedRows,
if (destroyedThisPass > 0) {
asteroidsRemainingCount = std::max(0, asteroidsRemainingCount - destroyedThisPass);
if (asteroidDestroyedCallback && lastDestroyedType.has_value()) {
asteroidDestroyedCallback(*lastDestroyedType);
}
}
}

View File

@ -88,8 +88,10 @@ public:
// Sound effect callbacks
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 AsteroidDestroyedCallback = std::function<void(AsteroidType)>; // Callback when an asteroid is fully destroyed
void setSoundCallback(SoundCallback callback) { soundCallback = callback; }
void setLevelUpCallback(LevelUpCallback callback) { levelUpCallback = callback; }
void setAsteroidDestroyedCallback(AsteroidDestroyedCallback callback) { asteroidDestroyedCallback = callback; }
// Shape helper --------------------------------------------------------
static bool cellFilled(const Piece& p, int cx, int cy);
@ -147,6 +149,7 @@ private:
// Sound effect callbacks
SoundCallback soundCallback;
LevelUpCallback levelUpCallback;
AsteroidDestroyedCallback asteroidDestroyedCallback;
// Gravity tuning -----------------------------------------------------
// Global multiplier applied to all level timings (use to slow/speed whole-game gravity)
double gravityGlobalMultiplier{1.0};

View File

@ -581,7 +581,11 @@ void GameRenderer::renderPlayingState(
float logicalH,
float logicalScale,
float winW,
float winH
float winH,
bool challengeClearFxActive,
const std::vector<int>* challengeClearFxOrder,
double challengeClearFxElapsedMs,
double challengeClearFxDurationMs
) {
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) {
float dropOffset = rowDropOffsets[y];
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));
}
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();
if (isAsteroid) {
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));
}
float size = finalBlockSize * spawnScale;
float size = finalBlockSize * spawnScale * clearScale;
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);
if (asteroidsTex && spawnAlpha < 1.0f) {
if (asteroidsTex && (spawnAlpha < 1.0f || clearAlpha < 1.0f)) {
SDL_SetTextureAlphaMod(asteroidsTex, 255);
}
} 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();
float activePiecePixelOffsetX = 0.0f;

View File

@ -1,5 +1,6 @@
#pragma once
#include <SDL3/SDL.h>
#include <vector>
#include "../../gameplay/core/Game.h"
// Forward declarations
@ -31,7 +32,11 @@ public:
float logicalH,
float logicalScale,
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)

View File

@ -221,11 +221,15 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
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;
bool shouldBlur = paused && !countdown && !challengeClearFx;
if (shouldBlur && m_renderTarget) {
// Render game to texture
@ -260,8 +264,12 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
1200.0f, // LOGICAL_W
1000.0f, // LOGICAL_H
logicalScale,
(float)winW,
(float)winH
(float)winW,
(float)winH,
challengeClearFx,
challengeClearOrder,
challengeClearElapsed,
challengeClearDuration
);
// Reset to screen
@ -351,7 +359,11 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
1000.0f,
logicalScale,
(float)winW,
(float)winH
(float)winH,
challengeClearFx,
challengeClearOrder,
challengeClearElapsed,
challengeClearDuration
);
}
}

View File

@ -67,6 +67,11 @@ struct StateContext {
int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default)
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
// 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
bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes