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]
|
||||
Music=1
|
||||
Sound=0
|
||||
Sound=1
|
||||
|
||||
[Gameplay]
|
||||
SmoothScroll=1
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
@ -261,7 +265,11 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
||||
1000.0f, // LOGICAL_H
|
||||
logicalScale,
|
||||
(float)winW,
|
||||
(float)winH
|
||||
(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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user