Merge branch 'feature/HardDropFx' into develop
This commit is contained in:
BIN
assets/music/hard_drop_001.mp3
Normal file
BIN
assets/music/hard_drop_001.mp3
Normal file
Binary file not shown.
BIN
assets/music/new_level.mp3
Normal file
BIN
assets/music/new_level.mp3
Normal file
Binary file not shown.
@ -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;
|
||||
}
|
||||
lockPiece();
|
||||
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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
@ -126,6 +141,18 @@ void GameRenderer::renderPlayingState(
|
||||
float winH
|
||||
) {
|
||||
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;
|
||||
@ -223,19 +250,132 @@ 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();
|
||||
|
||||
51
src/main.cpp
51
src/main.cpp
@ -807,7 +807,11 @@ 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;
|
||||
switch (linesCleared) {
|
||||
@ -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
|
||||
playVoiceCue(linesCleared);
|
||||
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;
|
||||
@ -1780,12 +1788,37 @@ int main(int, char **)
|
||||
SDL_SetRenderDrawColor(renderer, 255, 220, 0, 255);
|
||||
SDL_RenderRect(renderer, &inputRect);
|
||||
|
||||
// Player Name (with cursor)
|
||||
std::string display = playerName;
|
||||
if ((SDL_GetTicks() / 500) % 2 == 0) display += "_"; // Blink cursor
|
||||
|
||||
int nW=0, nH=0; pixelFont.measure(display, 1.2f, nW, nH);
|
||||
pixelFont.draw(renderer, inputX + (inputW - nW) * 0.5f + contentOffsetX, inputY + (inputH - nH) * 0.5f + contentOffsetY, display, 1.2f, {255, 255, 255, 255});
|
||||
// Player Name (blink cursor without shifting text)
|
||||
const float nameScale = 1.2f;
|
||||
const bool showCursor = ((SDL_GetTicks() / 500) % 2) == 0;
|
||||
|
||||
int metricsW = 0, metricsH = 0;
|
||||
pixelFont.measure("A", nameScale, metricsW, metricsH);
|
||||
if (metricsH == 0) metricsH = 24; // fallback height
|
||||
|
||||
int nameW = 0, nameH = 0;
|
||||
if (!playerName.empty()) {
|
||||
pixelFont.measure(playerName, nameScale, nameW, nameH);
|
||||
} else {
|
||||
nameH = metricsH;
|
||||
}
|
||||
|
||||
float textX = inputX + (inputW - static_cast<float>(nameW)) * 0.5f + contentOffsetX;
|
||||
float textY = inputY + (inputH - static_cast<float>(metricsH)) * 0.5f + contentOffsetY;
|
||||
|
||||
if (!playerName.empty()) {
|
||||
pixelFont.draw(renderer, textX, textY, playerName, nameScale, {255, 255, 255, 255});
|
||||
}
|
||||
|
||||
if (showCursor) {
|
||||
int cursorW = 0, cursorH = 0;
|
||||
pixelFont.measure("_", nameScale, cursorW, cursorH);
|
||||
float cursorX = playerName.empty()
|
||||
? inputX + (inputW - static_cast<float>(cursorW)) * 0.5f + contentOffsetX
|
||||
: textX + static_cast<float>(nameW);
|
||||
float cursorY = inputY + (inputH - static_cast<float>(cursorH)) * 0.5f + contentOffsetY;
|
||||
pixelFont.draw(renderer, cursorX, cursorY, "_", nameScale, {255, 255, 255, 255});
|
||||
}
|
||||
|
||||
// Hint
|
||||
const char* hint = "PRESS ENTER TO SUBMIT";
|
||||
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user