Added new level sound

This commit is contained in:
2025-11-30 10:37:35 +01:00
parent 69b2521695
commit 5fd77fdaf0
7 changed files with 219 additions and 5 deletions

Binary file not shown.

BIN
assets/music/new_level.mp3 Normal file

Binary file not shown.

View File

@ -57,6 +57,9 @@ void Game::reset(int startLevel_) {
// Initialize gravity using NES timing table (ms per cell by level)
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
fallAcc = 0; gameOver=false; paused=false;
hardDropShakeTimerMs = 0.0;
hardDropCells.clear();
hardDropFxId = 0;
_startTime = SDL_GetPerformanceCounter();
_pausedTime = 0;
_lastPauseStart = 0;
@ -328,6 +331,25 @@ void Game::softDropBoost(double frameMs) {
(void)frameMs;
}
void Game::updateVisualEffects(double frameMs) {
if (frameMs <= 0.0) {
return;
}
if (hardDropShakeTimerMs <= 0.0) {
hardDropShakeTimerMs = 0.0;
if (!hardDropCells.empty()) {
hardDropCells.clear();
}
return;
}
hardDropShakeTimerMs = std::max(0.0, hardDropShakeTimerMs - frameMs);
if (hardDropShakeTimerMs <= 0.0 && !hardDropCells.empty()) {
hardDropCells.clear();
}
}
void Game::hardDrop() {
if (paused) return;
// Count how many rows we drop for scoring parity with JS
@ -337,7 +359,34 @@ void Game::hardDrop() {
if (rows > 0) {
_score += rows * 1;
}
hardDropCells.clear();
hardDropCells.reserve(8);
for (int cy = 0; cy < 4; ++cy) {
for (int cx = 0; cx < 4; ++cx) {
if (!cellFilled(cur, cx, cy)) {
continue;
}
int gx = cur.x + cx;
int gy = cur.y + cy;
if (gx < 0 || gx >= COLS || gy >= ROWS) {
continue;
}
if (gy >= 0) {
hardDropCells.push_back(SDL_Point{gx, gy});
}
}
}
++hardDropFxId;
lockPiece();
hardDropShakeTimerMs = HARD_DROP_SHAKE_DURATION_MS;
}
double Game::hardDropShakeStrength() const {
if (hardDropShakeTimerMs <= 0.0) {
return 0.0;
}
return std::clamp(hardDropShakeTimerMs / HARD_DROP_SHAKE_DURATION_MS, 0.0, 1.0);
}
void Game::rotate(int dir) {

View File

@ -74,6 +74,13 @@ public:
double getFallAccumulator() const { return fallAcc; } // Debug: time accumulated toward next drop
void setLevelGravityMultiplier(int level, double m);
// Visual effect hooks
void updateVisualEffects(double frameMs);
bool hasHardDropShake() const { return hardDropShakeTimerMs > 0.0; }
double hardDropShakeStrength() const;
const std::vector<SDL_Point>& getHardDropCells() const { return hardDropCells; }
uint32_t getHardDropFxId() const { return hardDropFxId; }
private:
std::array<int, COLS*ROWS> board{}; // 0 empty else color index
Piece cur{}, hold{}, nextPiece{}; // current, held & next piece
@ -109,6 +116,12 @@ private:
// Backwards-compatible accessors (delegate to gravityMgr)
double computeGravityMsForLevel(int level) const;
// Impact FX timers
double hardDropShakeTimerMs{0.0};
static constexpr double HARD_DROP_SHAKE_DURATION_MS = 320.0;
std::vector<SDL_Point> hardDropCells;
uint32_t hardDropFxId{0};
// Internal helpers ----------------------------------------------------
void refillBag();
void spawn();

View File

@ -6,8 +6,23 @@
#include <array>
#include <cmath>
#include <cstdio>
#include <random>
#include <vector>
#include "../../core/Settings.h"
namespace {
struct ImpactSpark {
float x = 0.0f;
float y = 0.0f;
float vx = 0.0f;
float vy = 0.0f;
float lifeMs = 0.0f;
float maxLifeMs = 0.0f;
float size = 0.0f;
SDL_Color color{255, 255, 255, 255};
};
}
// Color constants (copied from main.cpp)
static const SDL_Color COLORS[] = {
{0, 0, 0, 255}, // 0: BLACK (empty)
@ -127,6 +142,18 @@ void GameRenderer::renderPlayingState(
) {
if (!game || !pixelFont) return;
static std::vector<ImpactSpark> s_impactSparks;
static uint32_t s_lastImpactFxId = 0;
static Uint32 s_lastImpactTick = SDL_GetTicks();
static std::mt19937 s_impactRng{ std::random_device{}() };
Uint32 nowTicks = SDL_GetTicks();
float sparkDeltaMs = static_cast<float>(nowTicks - s_lastImpactTick);
s_lastImpactTick = nowTicks;
if (sparkDeltaMs < 0.0f || sparkDeltaMs > 100.0f) {
sparkDeltaMs = 16.0f;
}
// Calculate actual content area (centered within the window)
float contentScale = logicalScale;
float contentW = logicalW * contentScale;
@ -223,20 +250,133 @@ void GameRenderer::renderPlayingState(
drawRectWithOffset(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255});
drawRectWithOffset(nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH, {30, 35, 50, 255});
// Precompute row drop offsets (line collapse effect)
std::array<float, Game::ROWS> rowDropOffsets{};
for (int y = 0; y < Game::ROWS; ++y) {
rowDropOffsets[y] = (lineEffect ? lineEffect->getRowDropOffset(y) : 0.0f);
}
// Draw the game board
const auto &board = game->boardRef();
float impactStrength = 0.0f;
float impactEased = 0.0f;
std::array<uint8_t, Game::COLS * Game::ROWS> impactMask{};
std::array<float, Game::COLS * Game::ROWS> impactWeight{};
if (game->hasHardDropShake()) {
impactStrength = static_cast<float>(game->hardDropShakeStrength());
impactStrength = std::clamp(impactStrength, 0.0f, 1.0f);
impactEased = impactStrength * impactStrength;
const auto& impactCells = game->getHardDropCells();
for (const auto& cell : impactCells) {
if (cell.x < 0 || cell.x >= Game::COLS || cell.y < 0 || cell.y >= Game::ROWS) {
continue;
}
int idx = cell.y * Game::COLS + cell.x;
impactMask[idx] = 1;
impactWeight[idx] = 1.0f;
int depth = 0;
for (int ny = cell.y + 1; ny < Game::ROWS && depth < 4; ++ny) {
if (board[ny * Game::COLS + cell.x] == 0) {
break;
}
++depth;
int nidx = ny * Game::COLS + cell.x;
impactMask[nidx] = 1;
float weight = std::max(0.15f, 1.0f - depth * 0.35f);
impactWeight[nidx] = std::max(impactWeight[nidx], weight);
}
}
}
bool shouldSpawnCrackles = game->hasHardDropShake() && !game->getHardDropCells().empty() && game->getHardDropFxId() != s_lastImpactFxId;
if (shouldSpawnCrackles) {
s_lastImpactFxId = game->getHardDropFxId();
std::uniform_real_distribution<float> jitter(-finalBlockSize * 0.2f, finalBlockSize * 0.2f);
std::uniform_real_distribution<float> velX(-0.04f, 0.04f);
std::uniform_real_distribution<float> velY(0.035f, 0.07f);
std::uniform_real_distribution<float> lifespan(210.0f, 320.0f);
std::uniform_real_distribution<float> sizeDist(finalBlockSize * 0.08f, finalBlockSize * 0.14f);
const auto& impactCells = game->getHardDropCells();
for (const auto& cell : impactCells) {
if (cell.x < 0 || cell.x >= Game::COLS || cell.y < 0 || cell.y >= Game::ROWS) {
continue;
}
int idx = cell.y * Game::COLS + cell.x;
int v = (cell.y >= 0) ? board[idx] : 0;
SDL_Color baseColor = (v > 0 && v < PIECE_COUNT + 1) ? COLORS[v] : SDL_Color{255, 220, 180, 255};
float cellX = gridX + (cell.x + 0.5f) * finalBlockSize;
float cellY = gridY + (cell.y + 0.85f) * finalBlockSize + rowDropOffsets[cell.y];
for (int i = 0; i < 4; ++i) {
ImpactSpark spark;
spark.x = cellX + jitter(s_impactRng);
spark.y = cellY + jitter(s_impactRng) * 0.25f;
spark.vx = velX(s_impactRng);
spark.vy = velY(s_impactRng);
spark.lifeMs = lifespan(s_impactRng);
spark.maxLifeMs = spark.lifeMs;
spark.size = sizeDist(s_impactRng);
spark.color = SDL_Color{
static_cast<Uint8>(std::min(255, baseColor.r + 30)),
static_cast<Uint8>(std::min(255, baseColor.g + 30)),
static_cast<Uint8>(std::min(255, baseColor.b + 30)),
255
};
s_impactSparks.push_back(spark);
}
}
}
for (int y = 0; y < Game::ROWS; ++y) {
float dropOffset = (lineEffect ? lineEffect->getRowDropOffset(y) : 0.0f);
float dropOffset = rowDropOffsets[y];
for (int x = 0; x < Game::COLS; ++x) {
int v = board[y * Game::COLS + x];
if (v > 0) {
float bx = gridX + x * finalBlockSize;
float by = gridY + y * finalBlockSize + dropOffset;
const int cellIdx = y * Game::COLS + x;
float weight = impactWeight[cellIdx];
if (impactStrength > 0.0f && weight > 0.0f && impactMask[cellIdx]) {
float cellSeed = static_cast<float>((x * 37 + y * 61) % 113);
float t = static_cast<float>(nowTicks % 10000) * 0.018f + cellSeed;
float amplitude = 6.0f * impactEased * weight;
float freq = 2.0f + weight * 1.3f;
bx += amplitude * std::sin(t * freq);
by += amplitude * 0.75f * std::cos(t * (freq + 1.1f));
}
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1);
}
}
}
if (!s_impactSparks.empty()) {
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
auto it = s_impactSparks.begin();
while (it != s_impactSparks.end()) {
ImpactSpark& spark = *it;
spark.vy += 0.00045f * sparkDeltaMs;
spark.x += spark.vx * sparkDeltaMs;
spark.y += spark.vy * sparkDeltaMs;
spark.lifeMs -= sparkDeltaMs;
if (spark.lifeMs <= 0.0f) {
it = s_impactSparks.erase(it);
continue;
}
float lifeRatio = spark.lifeMs / spark.maxLifeMs;
Uint8 alpha = static_cast<Uint8>(std::clamp(lifeRatio, 0.0f, 1.0f) * 160.0f);
SDL_SetRenderDrawColor(renderer, spark.color.r, spark.color.g, spark.color.b, alpha);
SDL_FRect sparkRect{
spark.x - spark.size * 0.5f,
spark.y - spark.size * 0.5f,
spark.size,
spark.size * 1.4f
};
SDL_RenderFillRect(renderer, &sparkRect);
++it;
}
}
bool allowActivePieceRender = true;
const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled();

View File

@ -807,6 +807,10 @@ int main(int, char **)
loadSoundWithFallback("boom_tetris", "boom_tetris");
loadSoundWithFallback("wonderful", "wonderful");
loadSoundWithFallback("lets_go", "lets_go"); // For level up
loadSoundWithFallback("hard_drop", "hard_drop_001");
loadSoundWithFallback("new_level", "new_level");
bool suppressLineVoiceForLevelUp = false;
auto playVoiceCue = [&](int linesCleared) {
const std::vector<std::string>* bank = nullptr;
@ -835,12 +839,16 @@ int main(int, char **)
SoundEffectManager::instance().playSound("clear_line", 1.0f);
// Layer a voiced callout based on the number of cleared lines
if (!suppressLineVoiceForLevelUp) {
playVoiceCue(linesCleared);
}
suppressLineVoiceForLevelUp = false;
});
game.setLevelUpCallback([&](int newLevel) {
// Play level up sound
SoundEffectManager::instance().playSound("lets_go", 1.0f); // Increased volume
SoundEffectManager::instance().playSound("new_level", 1.0f);
SoundEffectManager::instance().playSound("lets_go", 1.0f); // Existing voice line
suppressLineVoiceForLevelUp = true;
});
AppState state = AppState::Loading;

View File

@ -4,6 +4,7 @@
#include "../gameplay/effects/LineEffect.h"
#include "../persistence/Scores.h"
#include "../audio/Audio.h"
#include "../audio/SoundEffect.h"
#include "../graphics/renderers/GameRenderer.h"
#include "../core/Config.h"
#include <SDL3/SDL.h>
@ -115,6 +116,7 @@ void PlayingState::handleEvent(const SDL_Event& e) {
// Hard drop (space)
if (e.key.scancode == SDL_SCANCODE_SPACE) {
SoundEffectManager::instance().playSound("hard_drop", 0.7f);
ctx.game->hardDrop();
return;
}
@ -128,6 +130,8 @@ void PlayingState::handleEvent(const SDL_Event& e) {
void PlayingState::update(double frameMs) {
if (!ctx.game) return;
ctx.game->updateVisualEffects(frameMs);
// forward per-frame gameplay updates (gravity, line effects)
if (!ctx.game->isPaused()) {
ctx.game->tickGravity(frameMs);