Updated game speed

This commit is contained in:
2025-08-16 08:24:26 +02:00
parent d161b2c550
commit 71648fbaeb
20 changed files with 690 additions and 334 deletions

View File

@ -14,12 +14,45 @@ static const std::array<Shape, PIECE_COUNT> SHAPES = {{
Shape{ 0x02E0, 0x4460, 0x0E80, 0xC440 }, // L
}};
// NES (NTSC) gravity table: frames per grid cell for each level.
// Based on: 0-9, 10-12: 5, 13-15: 4, 16-18: 3, 19-28: 2, 29+: 1
namespace {
constexpr double NES_FPS = 60.0988;
constexpr double FRAME_MS = 1000.0 / NES_FPS;
struct LevelGravity { int framesPerCell; double levelMultiplier; };
// Default table following NES values; levelMultiplier starts at 1.0 and can be tuned per-level
LevelGravity LEVEL_TABLE[30] = {
{48,1.0}, {43,1.0}, {38,1.0}, {33,1.0}, {28,1.0}, {23,1.0}, {18,1.0}, {13,1.0}, {8,1.0}, {6,1.0},
{5,1.0}, {5,1.0}, {5,1.0}, {4,1.0}, {4,1.0}, {4,1.0}, {3,1.0}, {3,1.0}, {3,1.0}, {2,1.0},
{2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {1,1.0}
};
inline double gravityMsForLevel(int level, double globalMultiplier)
{
int idx = level < 0 ? 0 : (level >= 29 ? 29 : level);
const LevelGravity &lg = LEVEL_TABLE[idx];
double frames = lg.framesPerCell * lg.levelMultiplier;
double result = frames * FRAME_MS * globalMultiplier;
static int debug_calls = 0;
if (debug_calls < 3) {
printf("Level %d: %d frames per cell (mult %.2f) = %.1f ms per cell (global x%.2f)\\n",
level, lg.framesPerCell, lg.levelMultiplier, result, globalMultiplier);
debug_calls++;
}
return result;
}
}
void Game::reset(int startLevel_) {
std::fill(board.begin(), board.end(), 0);
std::fill(blockCounts.begin(), blockCounts.end(), 0);
bag.clear();
_score = 0; _lines = 0; _level = startLevel_ + 1; startLevel = startLevel_;
gravityMs = 800.0 * std::pow(0.85, startLevel_); // speed-up for higher starts
_score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_;
// Initialize gravity using NES timing table (ms per cell by level)
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
fallAcc = 0; _elapsedSec = 0; gameOver=false; paused=false;
hold = Piece{}; hold.type = PIECE_COUNT; canHold=true;
refillBag(); spawn();
@ -31,6 +64,18 @@ void Game::refillBag() {
std::shuffle(bag.begin(), bag.end(), rng);
}
double Game::getGravityGlobalMultiplier() const { return gravityGlobalMultiplier; }
double Game::getGravityMs() const { return gravityMs; }
void Game::setLevelGravityMultiplier(int level, double m) {
if (level < 0) return;
int idx = level >= 29 ? 29 : level;
LEVEL_TABLE[idx].levelMultiplier = m;
// If current level changed, refresh gravityMs
if (_level == idx) gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
}
void Game::spawn() {
if (bag.empty()) refillBag();
PieceType pieceType = bag.back();
@ -97,14 +142,15 @@ void Game::lockPiece() {
case 4: base = 1200; break; // TETRIS
default: base = 0; break;
}
_score += base * std::max(1, _level);
// multiplier is level+1 to match original scoring where level 0 => x1
_score += base * (_level + 1);
// Update total lines
_lines += cleared;
// JS level progression (NES-like) using starting level rules
// startLevel is 0-based in JS; our _level is JS level + 1
const int threshold = (startLevel + 1) * 10;
// Both startLevel and _level are 0-based now.
const int threshold = (startLevel + 1) * 10; // first promotion after this many lines
int oldLevel = _level;
// First level up happens when total lines equal threshold
// After that, every 10 lines (when (lines - threshold) % 10 == 0)
@ -115,10 +161,9 @@ void Game::lockPiece() {
}
if (_level > oldLevel) {
gravityMs = std::max(60.0, gravityMs * 0.85);
if (levelUpCallback) {
levelUpCallback(_level);
}
// Update gravity to exact NES speed for the new level
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
if (levelUpCallback) levelUpCallback(_level);
}
// Trigger sound effect callback for line clears
@ -210,7 +255,8 @@ void Game::tickGravity(double frameMs) {
}
void Game::softDropBoost(double frameMs) {
if (!paused) fallAcc += frameMs * 10.0;
// Reduce soft drop speed multiplier from 10.0 to 3.0 to make it less aggressive
if (!paused) fallAcc += frameMs * 3.0;
}
void Game::hardDrop() {
@ -227,8 +273,34 @@ void Game::hardDrop() {
void Game::rotate(int dir) {
if (paused) return;
Piece p = cur; p.rot = (p.rot + dir + 4) % 4; const int kicks[5]={0,-1,1,-2,2};
for (int dx : kicks) { p.x = cur.x + dx; if (!collides(p)) { cur = p; return; } }
Piece p = cur;
p.rot = (p.rot + dir + 4) % 4;
// Try rotation at current position first
if (!collides(p)) {
cur = p;
return;
}
// JavaScript-style wall kicks: try horizontal, up, then larger horizontal offsets
const std::pair<int,int> kicks[] = {
{1, 0}, // right
{-1, 0}, // left
{0, -1}, // up (key difference from our previous approach)
{2, 0}, // 2 right (for I piece)
{-2, 0}, // 2 left (for I piece)
};
for (auto kick : kicks) {
Piece test = p;
test.x = cur.x + kick.first;
test.y = cur.y + kick.second;
if (!collides(test)) {
cur = test;
return;
}
}
}
void Game::move(int dx) {

View File

@ -62,6 +62,12 @@ public:
// Shape helper --------------------------------------------------------
static bool cellFilled(const Piece& p, int cx, int cy);
// Gravity tuning accessors (public API for HUD/tuning)
void setGravityGlobalMultiplier(double m) { gravityGlobalMultiplier = m; }
double getGravityGlobalMultiplier() const;
double getGravityMs() const;
void setLevelGravityMultiplier(int level, double m);
private:
std::array<int, COLS*ROWS> board{}; // 0 empty else color index
Piece cur{}, hold{}, nextPiece{}; // current, held & next piece
@ -87,6 +93,9 @@ private:
// Sound effect callbacks
SoundCallback soundCallback;
LevelUpCallback levelUpCallback;
// Gravity tuning -----------------------------------------------------
// Global multiplier applied to all level timings (use to slow/speed whole-game gravity)
double gravityGlobalMultiplier{2.8};
// Internal helpers ----------------------------------------------------
void refillBag();
@ -96,4 +105,5 @@ private:
int checkLines(); // Find completed lines and store them
void actualClearLines(); // Actually remove lines from board
bool tryMoveDown(); // one-row fall; returns true if moved
// Gravity tuning helpers (public API declared above)
};

View File

@ -284,10 +284,10 @@ void LineEffect::playLineClearSound(int lineCount) {
if (lineCount == 4) {
sample = &tetrisSample; // Special sound for Tetris
printf("TETRIS! 4 lines cleared!\n");
//printf("TETRIS! 4 lines cleared!\n");
} else {
sample = &lineClearSample; // Regular line clear sound
printf("Line clear: %d lines\n", lineCount);
//printf("Line clear: %d lines\n", lineCount);
}
if (sample && !sample->empty()) {

View File

@ -43,8 +43,7 @@ bool SoundEffect::load(const std::string& filePath) {
}
loaded = true;
std::printf("[SoundEffect] Loaded: %s (%d channels, %d Hz, %zu samples)\n",
filePath.c_str(), channels, sampleRate, pcmData.size());
//std::printf("[SoundEffect] Loaded: %s (%d channels, %d Hz, %zu samples)\n", filePath.c_str(), channels, sampleRate, pcmData.size());
return true;
}
@ -54,7 +53,7 @@ void SoundEffect::play(float volume) {
return;
}
std::printf("[SoundEffect] Playing sound with %zu samples at volume %.2f\n", pcmData.size(), volume);
//std::printf("[SoundEffect] Playing sound with %zu samples at volume %.2f\n", pcmData.size(), volume);
// Calculate final volume
float finalVolume = defaultVolume * volume;
@ -80,13 +79,13 @@ bool SimpleAudioPlayer::init() {
}
initialized = true;
std::printf("[SimpleAudioPlayer] Initialized\n");
//std::printf("[SimpleAudioPlayer] Initialized\n");
return true;
}
void SimpleAudioPlayer::shutdown() {
initialized = false;
std::printf("[SimpleAudioPlayer] Shut down\n");
//std::printf("[SimpleAudioPlayer] Shut down\n");
}
void SimpleAudioPlayer::playSound(const std::vector<int16_t>& pcmData, int channels, int sampleRate, float volume) {
@ -244,7 +243,7 @@ bool SoundEffectManager::init() {
SimpleAudioPlayer::instance().init();
initialized = true;
std::printf("[SoundEffectManager] Initialized\n");
//std::printf("[SoundEffectManager] Initialized\n");
return true;
}
@ -252,7 +251,7 @@ void SoundEffectManager::shutdown() {
soundEffects.clear();
SimpleAudioPlayer::instance().shutdown();
initialized = false;
std::printf("[SoundEffectManager] Shut down\n");
//std::printf("[SoundEffectManager] Shut down\n");
}
bool SoundEffectManager::loadSound(const std::string& id, const std::string& filePath) {
@ -274,7 +273,7 @@ bool SoundEffectManager::loadSound(const std::string& id, const std::string& fil
soundEffects.end());
soundEffects.emplace_back(id, std::move(soundEffect));
std::printf("[SoundEffectManager] Loaded sound '%s' from %s\n", id.c_str(), filePath.c_str());
//std::printf("[SoundEffectManager] Loaded sound '%s' from %s\n", id.c_str(), filePath.c_str());
return true;
}

5
src/app_icon.rc Normal file
View File

@ -0,0 +1,5 @@
/* Windows resource script to embed the application icon */
/* Uses the project's favicon icon located in assets/favicon/favicon.ico */
// Simple icon resource - the icon will be copied to the build directory by CMake
1 ICON "favicon.ico"

View File

@ -26,6 +26,9 @@
#include "states/State.h"
#include "states/LoadingState.h"
#include "states/MenuState.h"
#include "states/PlayingState.h"
// Debug logging removed: no-op in this build (previously LOG_DEBUG)
// Font rendering now handled by FontAtlas
@ -89,6 +92,43 @@ static void drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx
font.draw(renderer, textX, textY, label, textScale, {255, 255, 255, 255});
}
// External wrapper for enhanced button so other translation units can call it.
void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, bool isHovered, bool isSelected) {
drawEnhancedButton(renderer, font, cx, cy, w, h, label, isHovered, isSelected);
}
// Popup wrappers
// Forward declarations for popup functions defined later in this file
static void drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel);
static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);
void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel) {
drawLevelSelectionPopup(renderer, font, bgTex, selectedLevel);
}
void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) {
drawSettingsPopup(renderer, font, musicEnabled);
}
// Simple rounded menu button drawer used by MenuState (keeps visual parity with JS)
void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, SDL_Color bgColor, SDL_Color borderColor) {
float x = cx - w/2;
float y = cy - h/2;
drawRect(renderer, x-6, y-6, w+12, h+12, borderColor);
drawRect(renderer, x-4, y-4, w+8, h+8, {255,255,255,255});
drawRect(renderer, x, y, w, h, bgColor);
float textScale = 1.6f;
float approxCharW = 12.0f * textScale;
float textW = label.length() * approxCharW;
float tx = x + (w - textW) / 2.0f;
float ty = y + (h - 20.0f * textScale) / 2.0f;
font.draw(renderer, tx+2, ty+2, label, textScale, {0,0,0,180});
font.draw(renderer, tx, ty, label, textScale, {255,255,255,255});
}
// -----------------------------------------------------------------------------
// Block Drawing Functions
// -----------------------------------------------------------------------------
@ -98,7 +138,7 @@ static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, flo
if (!blocksTex) {
static bool printed = false;
if (!printed) {
printf("drawBlockTexture: No texture available, using colored rectangles\n");
(void)0;
printed = true;
}
}
@ -184,47 +224,69 @@ static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, Piece
// -----------------------------------------------------------------------------
// Popup Drawing Functions
// -----------------------------------------------------------------------------
static void drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, int selectedLevel) {
float popupW = 400, popupH = 300;
float popupX = (LOGICAL_W - popupW) / 2;
float popupY = (LOGICAL_H - popupH) / 2;
// Semi-transparent overlay
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 128);
SDL_FRect overlay{0, 0, LOGICAL_W, LOGICAL_H};
SDL_RenderFillRect(renderer, &overlay);
// Popup background
drawRect(renderer, popupX-4, popupY-4, popupW+8, popupH+8, {100, 120, 160, 255}); // Border
drawRect(renderer, popupX, popupY, popupW, popupH, {40, 50, 70, 255}); // Background
// Title
font.draw(renderer, popupX + 20, popupY + 20, "SELECT STARTING LEVEL", 2.0f, {255, 220, 0, 255});
// Level grid (4x5 = 20 levels, 0-19)
float gridStartX = popupX + 50;
float gridStartY = popupY + 70;
float cellW = 70, cellH = 35;
for (int level = 0; level < 20; level++) {
int row = level / 4;
int col = level % 4;
float cellX = gridStartX + col * cellW;
float cellY = gridStartY + row * cellH;
bool isSelected = (level == selectedLevel);
SDL_Color cellColor = isSelected ? SDL_Color{255, 220, 0, 255} : SDL_Color{80, 100, 140, 255};
SDL_Color textColor = isSelected ? SDL_Color{0, 0, 0, 255} : SDL_Color{255, 255, 255, 255};
drawRect(renderer, cellX, cellY, cellW-5, cellH-5, cellColor);
char levelStr[8];
snprintf(levelStr, sizeof(levelStr), "%d", level);
font.draw(renderer, cellX + 25, cellY + 8, levelStr, 1.2f, textColor);
static void drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel) {
// Popup dims scale with logical size for responsiveness
float popupW = std::min(760.0f, LOGICAL_W * 0.75f);
float popupH = std::min(520.0f, LOGICAL_H * 0.7f);
float popupX = (LOGICAL_W - popupW) / 2.0f;
float popupY = (LOGICAL_H - popupH) / 2.0f;
// Draw the background picture stretched to full logical viewport if available
if (bgTex) {
// Dim the background by rendering it then overlaying a semi-transparent black rect
SDL_FRect dst{0, 0, (float)LOGICAL_W, (float)LOGICAL_H};
SDL_RenderTexture(renderer, bgTex, nullptr, &dst);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 160);
SDL_FRect dim{0,0,(float)LOGICAL_W,(float)LOGICAL_H};
SDL_RenderFillRect(renderer, &dim);
} else {
// Fallback to semi-transparent overlay
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180);
SDL_FRect overlay{0, 0, (float)LOGICAL_W, (float)LOGICAL_H};
SDL_RenderFillRect(renderer, &overlay);
}
// Instructions
font.draw(renderer, popupX + 20, popupY + 230, "CLICK TO SELECT • ESC TO CANCEL", 1.0f, {200, 200, 220, 255});
// Popup panel with border and subtle background
drawRect(renderer, popupX-6, popupY-6, popupW+12, popupH+12, {90, 110, 140, 200}); // outer border
drawRect(renderer, popupX-3, popupY-3, popupW+6, popupH+6, {30, 38, 60, 220}); // inner border
drawRect(renderer, popupX, popupY, popupW, popupH, {18, 22, 34, 235}); // panel
// Title (use retro pixel font)
font.draw(renderer, popupX + 28, popupY + 18, "SELECT STARTING LEVEL", 2.4f, {255, 220, 0, 255});
// Grid layout for levels: 4 columns x 5 rows
int cols = 4, rows = 5;
float padding = 24.0f;
float gridW = popupW - padding * 2;
float gridH = popupH - 120.0f; // leave space for title and instructions
float cellW = gridW / cols;
float cellH = std::min(80.0f, gridH / rows - 12.0f);
float gridStartX = popupX + padding;
float gridStartY = popupY + 70;
for (int level = 0; level < 20; ++level) {
int row = level / cols;
int col = level % cols;
float cx = gridStartX + col * cellW;
float cy = gridStartY + row * (cellH + 12.0f);
bool isSelected = (level == selectedLevel);
SDL_Color bg = isSelected ? SDL_Color{255, 220, 0, 255} : SDL_Color{70, 85, 120, 240};
SDL_Color fg = isSelected ? SDL_Color{0, 0, 0, 255} : SDL_Color{240, 240, 245, 255};
// Button background
drawRect(renderer, cx + 8, cy, cellW - 16, cellH, bg);
// Level label centered
char levelStr[8]; snprintf(levelStr, sizeof(levelStr), "%d", level);
float tx = cx + (cellW / 2.0f) - (6.0f * 1.8f); // rough centering
float ty = cy + (cellH / 2.0f) - 10.0f;
font.draw(renderer, tx, ty, levelStr, 1.8f, fg);
}
// Instructions under grid
font.draw(renderer, popupX + 28, popupY + popupH - 40, "CLICK A LEVEL TO SELECT • ESC = CANCEL", 1.0f, {200,200,220,255});
}
static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) {
@ -279,92 +341,90 @@ static bool showLevelPopup = false;
static bool showSettingsPopup = false;
static bool musicEnabled = true;
static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings
// Shared texture for fireworks particles (uses the blocks sheet)
static SDL_Texture* fireworksBlocksTex = nullptr;
// -----------------------------------------------------------------------------
// Tetris Block Fireworks for intro animation
// Tetris Block Fireworks for intro animation (block particles)
// Forward declare block render helper used by particles
static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType);
// -----------------------------------------------------------------------------
struct TetrisParticle {
struct BlockParticle {
float x, y, vx, vy, size, alpha, decay;
SDL_Color color;
bool hasTrail;
std::vector<std::pair<float, float>> trail;
TetrisParticle(float x_, float y_, SDL_Color color_)
: x(x_), y(y_), color(color_), hasTrail(false) {
float angle = (rand() % 628) / 100.0f; // 0 to 2π
float speed = 1 + (rand() % 400) / 100.0f; // 1 to 5
vx = cos(angle) * speed;
vy = sin(angle) * speed;
size = 1 + (rand() % 200) / 100.0f; // 1 to 3
int blockType; // 0..6
BlockParticle(float sx, float sy)
: x(sx), y(sy) {
float angle = (rand() % 628) / 100.0f; // 0..2pi
float speed = 1.5f + (rand() % 350) / 100.0f; // ~1.5..5.0
vx = std::cos(angle) * speed;
vy = std::sin(angle) * speed;
size = 6.0f + (rand() % 50) / 10.0f; // 6..11 px
alpha = 1.0f;
decay = 0.01f + (rand() % 200) / 10000.0f; // 0.01 to 0.03
hasTrail = (rand() % 100) < 30; // 30% chance
decay = 0.012f + (rand() % 200) / 10000.0f; // 0.012..0.032
blockType = rand() % 7; // choose a tetris color
}
bool update() {
if (hasTrail) {
trail.push_back({x, y});
if (trail.size() > 5) trail.erase(trail.begin());
}
vx *= 0.98f; // friction
vy = vy * 0.98f + 0.06f; // gravity
vx *= 0.985f; // friction
vy = vy * 0.985f + 0.07f; // gravity
x += vx;
y += vy;
alpha -= decay;
if (size > 0.1f) size -= 0.03f;
return alpha > 0;
size = std::max(2.0f, size - 0.04f);
return alpha > 0.02f;
}
};
struct TetrisFirework {
std::vector<TetrisParticle> particles;
std::vector<BlockParticle> particles;
int mode = 0; // 0=random,1=red,2=green,3=palette
TetrisFirework(float x, float y) {
SDL_Color colors[] = {
{255, 255, 0, 255}, // Yellow
{0, 255, 255, 255}, // Cyan
{255, 0, 255, 255}, // Magenta
{0, 255, 0, 255}, // Green
{255, 0, 0, 255}, // Red
{0, 0, 255, 255}, // Blue
{255, 160, 0, 255} // Orange
};
int particleCount = 20 + rand() % 21; // 20-40 particles
for (int i = 0; i < particleCount; i++) {
SDL_Color color = colors[rand() % 7];
particles.emplace_back(x, y, color);
}
mode = rand() % 4;
int particleCount = 30 + rand() % 25; // 30-55 particles
particles.reserve(particleCount);
for (int i = 0; i < particleCount; ++i) particles.emplace_back(x, y);
}
bool update() {
for (auto it = particles.begin(); it != particles.end();) {
if (!it->update()) {
it = particles.erase(it);
} else {
++it;
}
if (!it->update()) it = particles.erase(it); else ++it;
}
return !particles.empty();
}
void draw(SDL_Renderer* renderer) {
for (auto& p : particles) {
// Draw trail
if (p.hasTrail && p.trail.size() > 1) {
for (size_t i = 1; i < p.trail.size(); i++) {
float trailAlpha = p.alpha * 0.3f * (float(i) / p.trail.size());
SDL_SetRenderDrawColor(renderer, p.color.r, p.color.g, p.color.b, Uint8(trailAlpha * 255));
SDL_RenderLine(renderer, p.trail[i-1].first, p.trail[i-1].second, p.trail[i].first, p.trail[i].second);
for (auto &p : particles) {
if (fireworksBlocksTex) {
// Apply per-particle alpha and color variants by modulating the blocks texture
// Save previous mods (assume single-threaded rendering)
Uint8 prevA = 255;
SDL_GetTextureAlphaMod(fireworksBlocksTex, &prevA);
Uint8 setA = Uint8(std::max(0.0f, std::min(1.0f, p.alpha)) * 255.0f);
SDL_SetTextureAlphaMod(fireworksBlocksTex, setA);
// Color modes: tint the texture where appropriate
if (mode == 1) {
// red
SDL_SetTextureColorMod(fireworksBlocksTex, 220, 60, 60);
} else if (mode == 2) {
// green
SDL_SetTextureColorMod(fireworksBlocksTex, 80, 200, 80);
} else if (mode == 3) {
// tint to the particle's block color
SDL_Color c = COLORS[p.blockType + 1];
SDL_SetTextureColorMod(fireworksBlocksTex, c.r, c.g, c.b);
} else {
// random: no tint (use texture colors directly)
SDL_SetTextureColorMod(fireworksBlocksTex, 255, 255, 255);
}
drawBlockTexture(renderer, fireworksBlocksTex, p.x - p.size * 0.5f, p.y - p.size * 0.5f, p.size, p.blockType);
// Restore alpha and color modulation
SDL_SetTextureAlphaMod(fireworksBlocksTex, prevA);
SDL_SetTextureColorMod(fireworksBlocksTex, 255, 255, 255);
} else {
SDL_SetRenderDrawColor(renderer, 255, 255, 255, Uint8(p.alpha * 255));
SDL_FRect rect{p.x - p.size/2, p.y - p.size/2, p.size, p.size};
SDL_RenderFillRect(renderer, &rect);
}
// Draw particle
SDL_SetRenderDrawColor(renderer, p.color.r, p.color.g, p.color.b, Uint8(p.alpha * 255));
SDL_FRect rect{p.x - p.size/2, p.y - p.size/2, p.size, p.size};
SDL_RenderFillRect(renderer, &rect);
}
}
};
@ -377,11 +437,10 @@ static Uint64 lastFireworkTime = 0;
// -----------------------------------------------------------------------------
static void updateFireworks(double frameMs) {
Uint64 now = SDL_GetTicks();
// Randomly spawn new fireworks (2% chance per frame)
if (fireworks.size() < 6 && (rand() % 100) < 2) {
float x = 100 + rand() % (LOGICAL_W - 200);
float y = 100 + rand() % (LOGICAL_H - 300);
// Randomly spawn new block fireworks (2% chance per frame), bias to lower-right
if (fireworks.size() < 5 && (rand() % 100) < 2) {
float x = LOGICAL_W * 0.55f + float(rand() % int(LOGICAL_W * 0.35f));
float y = LOGICAL_H * 0.80f + float(rand() % int(LOGICAL_H * 0.15f));
fireworks.emplace_back(x, y);
lastFireworkTime = now;
}
@ -397,22 +456,29 @@ static void updateFireworks(double frameMs) {
}
static void drawFireworks(SDL_Renderer* renderer) {
for (auto& firework : fireworks) {
firework.draw(renderer);
}
for (auto& f : fireworks) f.draw(renderer);
}
// External wrappers for use by other translation units (MenuState)
// These call the internal helpers above so we don't change existing static linkage.
void menu_drawFireworks(SDL_Renderer* renderer) { drawFireworks(renderer); }
void menu_updateFireworks(double frameMs) { updateFireworks(frameMs); }
double menu_getLogoAnimCounter() { return logoAnimCounter; }
int menu_getHoveredButton() { return hoveredButton; }
int main(int, char **)
{
// Initialize random seed for fireworks
srand(static_cast<unsigned int>(SDL_GetTicks()));
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0)
int sdlInitRes = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
if (sdlInitRes < 0)
{
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
return 1;
}
if (TTF_Init() < 0)
int ttfInitRes = TTF_Init();
if (ttfInitRes < 0)
{
std::fprintf(stderr, "TTF_Init failed\n");
SDL_Quit();
@ -457,43 +523,61 @@ int main(int, char **)
// Load logo using native SDL BMP loading
SDL_Texture *logoTex = nullptr;
SDL_Surface* logoSurface = SDL_LoadBMP("assets/images/logo_small.bmp");
SDL_Surface* logoSurface = SDL_LoadBMP("assets/images/logo.bmp");
if (logoSurface) {
printf("Successfully loaded logo_small.bmp using native SDL\n");
(void)0;
logoTex = SDL_CreateTextureFromSurface(renderer, logoSurface);
SDL_DestroySurface(logoSurface);
} else {
printf("Warning: logo_small.bmp not found\n");
(void)0;
}
// Load small logo (used by Menu to show whole logo)
SDL_Texture *logoSmallTex = nullptr;
SDL_Surface* logoSmallSurface = SDL_LoadBMP("assets/images/logo_small.bmp");
int logoSmallW = 0, logoSmallH = 0;
if (logoSmallSurface) {
// capture surface size before creating the texture (avoids SDL_QueryTexture)
logoSmallW = logoSmallSurface->w;
logoSmallH = logoSmallSurface->h;
logoSmallTex = SDL_CreateTextureFromSurface(renderer, logoSmallSurface);
SDL_DestroySurface(logoSmallSurface);
} else {
// fallback: leave logoSmallTex null so MenuState will use large logo
(void)0;
}
// Load background using native SDL BMP loading
SDL_Texture *backgroundTex = nullptr;
SDL_Surface* backgroundSurface = SDL_LoadBMP("assets/images/main_background.bmp");
if (backgroundSurface) {
printf("Successfully loaded main_background.bmp using native SDL\n");
(void)0;
backgroundTex = SDL_CreateTextureFromSurface(renderer, backgroundSurface);
SDL_DestroySurface(backgroundSurface);
} else {
printf("Warning: main_background.bmp not found\n");
(void)0;
}
// Level background caching system
SDL_Texture *levelBackgroundTex = nullptr;
SDL_Texture *nextLevelBackgroundTex = nullptr; // used during fade transitions
int cachedLevel = -1; // Track which level background is currently cached
float levelFadeAlpha = 0.0f; // 0..1 blend factor where 1 means next fully visible
const float LEVEL_FADE_DURATION = 3500.0f; // ms for fade transition (3.5s)
float levelFadeElapsed = 0.0f;
// Load blocks texture using native SDL BMP loading
SDL_Texture *blocksTex = nullptr;
SDL_Surface* blocksSurface = SDL_LoadBMP("assets/images/blocks90px_001.bmp");
if (blocksSurface) {
printf("Successfully loaded blocks90px_001.bmp using native SDL\n");
(void)0;
blocksTex = SDL_CreateTextureFromSurface(renderer, blocksSurface);
SDL_DestroySurface(blocksSurface);
} else {
printf("Warning: blocks90px_001.bmp not found, creating programmatic texture...\n");
(void)0;
}
if (!blocksTex) {
printf("All image formats failed, creating blocks texture programmatically...\n");
(void)0;
// Create a 630x90 texture (7 blocks * 90px each)
blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90);
@ -521,14 +605,18 @@ int main(int, char **)
// Reset render target
SDL_SetRenderTarget(renderer, nullptr);
printf("Successfully created programmatic blocks texture\n");
(void)0;
} else {
printf("Failed to create programmatic texture: %s\n", SDL_GetError());
std::fprintf(stderr, "Failed to create programmatic texture: %s\n", SDL_GetError());
}
} else {
printf("Successfully loaded PNG blocks texture\n");
(void)0;
}
// Provide the blocks sheet to the fireworks system (for block particles)
fireworksBlocksTex = blocksTex;
// Default start level selection: 0
int startLevelSelection = 0;
Game game(startLevelSelection);
@ -553,7 +641,7 @@ int main(int, char **)
if (wavFile) {
SDL_CloseIO(wavFile);
if (SoundEffectManager::instance().loadSound(id, wavPath)) {
printf("Loaded WAV: %s\n", wavPath.c_str());
(void)0;
return;
}
}
@ -563,12 +651,12 @@ int main(int, char **)
if (mp3File) {
SDL_CloseIO(mp3File);
if (SoundEffectManager::instance().loadSound(id, mp3Path)) {
printf("Loaded MP3: %s\n", mp3Path.c_str());
(void)0;
return;
}
}
printf("Failed to load sound: %s (tried both WAV and MP3)\n", id.c_str());
std::fprintf(stderr, "Failed to load sound: %s (tried both WAV and MP3)\n", id.c_str());
};
loadSoundWithFallback("nice_combo", "nice_combo");
@ -634,15 +722,21 @@ int main(int, char **)
ctx.pixelFont = &pixelFont;
ctx.lineEffect = &lineEffect;
ctx.logoTex = logoTex;
ctx.logoSmallTex = logoSmallTex;
ctx.logoSmallW = logoSmallW;
ctx.logoSmallH = logoSmallH;
ctx.backgroundTex = backgroundTex;
ctx.blocksTex = blocksTex;
ctx.musicEnabled = &musicEnabled;
ctx.startLevelSelection = &startLevelSelection;
ctx.hoveredButton = &hoveredButton;
ctx.showLevelPopup = &showLevelPopup;
ctx.showSettingsPopup = &showSettingsPopup;
// Instantiate state objects
auto loadingState = std::make_unique<LoadingState>(ctx);
auto menuState = std::make_unique<MenuState>(ctx);
auto playingState = std::make_unique<PlayingState>(ctx);
// Register handlers and lifecycle hooks
stateMgr.registerHandler(AppState::Loading, [&](const SDL_Event& e){ loadingState->handleEvent(e); });
@ -653,29 +747,31 @@ int main(int, char **)
stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); });
stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); });
// Minimal Playing state handler (gameplay input) - kept inline until PlayingState is implemented
// Combined Playing state handler: run playingState handler and inline gameplay mapping
stateMgr.registerHandler(AppState::Playing, [&](const SDL_Event& e){
// First give the PlayingState a chance to handle the event
playingState->handleEvent(e);
// Then perform inline gameplay mappings (gravity/rotation/hard-drop/hold)
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
if (!game.isPaused()) {
// Hard drop (Space)
if (e.key.scancode == SDL_SCANCODE_SPACE) {
game.hardDrop();
}
// Rotate clockwise (Up arrow)
else if (e.key.scancode == SDL_SCANCODE_UP) {
game.rotate(+1);
}
// Rotate counter-clockwise (Z) or Shift modifier
else if (e.key.scancode == SDL_SCANCODE_Z || (e.key.mod & SDL_KMOD_SHIFT)) {
game.rotate(-1);
}
// Hold current piece (C) or Ctrl modifier
else if (e.key.scancode == SDL_SCANCODE_C || (e.key.mod & SDL_KMOD_CTRL)) {
game.holdCurrent();
}
}
}
});
stateMgr.registerOnEnter(AppState::Playing, [&](){ playingState->onEnter(); });
stateMgr.registerOnExit(AppState::Playing, [&](){ playingState->onExit(); });
// Playing, LevelSelect and GameOver currently use inline logic in main; we'll migrate later
while (running)
@ -736,6 +832,12 @@ int main(int, char **)
float lx = (mx - logicalVP.x) / logicalScale, ly = (my - logicalVP.y) / logicalScale;
if (state == AppState::Menu)
{
// Compute content offsets (match MenuState centering)
float contentW = LOGICAL_W * logicalScale;
float contentH = LOGICAL_H * logicalScale;
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
if (showLevelPopup) {
// Handle level selection popup clicks
float popupW = 400, popupH = 300;
@ -767,15 +869,23 @@ int main(int, char **)
// Click anywhere closes settings popup
showSettingsPopup = false;
} else {
// Main menu buttons
SDL_FRect playBtn{LOGICAL_W * 0.5f - 100, LOGICAL_H * 0.75f, 200, 50};
SDL_FRect levelBtn{LOGICAL_W * 0.5f - 100, LOGICAL_H * 0.75f + 60, 200, 50};
// Responsive Main menu buttons (match MenuState layout)
bool isSmall = ((LOGICAL_W * logicalScale) < 700.0f);
float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f;
float btnH = isSmall ? 60.0f : 70.0f;
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
const float btnYOffset = 40.0f; // must match MenuState offset
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h)
{
// Reset the game first with the chosen start level so HUD and
// Playing state see the correct 0-based level immediately.
game.reset(startLevelSelection);
state = AppState::Playing;
stateMgr.setState(state);
game.reset(startLevelSelection);
}
else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h)
{
@ -806,9 +916,19 @@ int main(int, char **)
float lx = (mx - logicalVP.x) / logicalScale, ly = (my - logicalVP.y) / logicalScale;
if (state == AppState::Menu && !showLevelPopup && !showSettingsPopup)
{
// Check button hover states
SDL_FRect playBtn{LOGICAL_W * 0.5f - 100, LOGICAL_H * 0.75f, 200, 50};
SDL_FRect levelBtn{LOGICAL_W * 0.5f - 100, LOGICAL_H * 0.75f + 60, 200, 50};
// Compute content offsets and responsive buttons (match MenuState)
float contentW = LOGICAL_W * logicalScale;
float contentH = LOGICAL_H * logicalScale;
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
bool isSmall = ((LOGICAL_W * logicalScale) < 700.0f);
float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f;
float btnH = isSmall ? 60.0f : 70.0f;
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
const float btnYOffset = 40.0f; // must match MenuState offset
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
hoveredButton = -1;
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h)
@ -973,6 +1093,12 @@ int main(int, char **)
} else {
starfield.update(float(frameMs / 1000.0f), logicalVP.x * 2 + logicalVP.w, logicalVP.y * 2 + logicalVP.h);
}
// Advance level background fade if a next texture is queued
if (nextLevelBackgroundTex) {
levelFadeElapsed += float(frameMs);
levelFadeAlpha = std::min(1.0f, levelFadeElapsed / LEVEL_FADE_DURATION);
}
// Update intro animations
if (state == AppState::Menu) {
@ -988,6 +1114,9 @@ int main(int, char **)
case AppState::Menu:
menuState->update(frameMs);
break;
case AppState::Playing:
playingState->update(frameMs);
break;
default:
break;
}
@ -1005,41 +1134,64 @@ int main(int, char **)
// Only load new background if level changed
if (cachedLevel != bgLevel) {
// Clean up old texture
if (levelBackgroundTex) {
SDL_DestroyTexture(levelBackgroundTex);
levelBackgroundTex = nullptr;
}
// Load new level background
// Load new level background into nextLevelBackgroundTex
if (nextLevelBackgroundTex) { SDL_DestroyTexture(nextLevelBackgroundTex); nextLevelBackgroundTex = nullptr; }
char bgPath[256];
snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.bmp", bgLevel);
SDL_Surface* levelBgSurface = SDL_LoadBMP(bgPath);
if (levelBgSurface) {
levelBackgroundTex = SDL_CreateTextureFromSurface(renderer, levelBgSurface);
nextLevelBackgroundTex = SDL_CreateTextureFromSurface(renderer, levelBgSurface);
SDL_DestroySurface(levelBgSurface);
// start fade transition
levelFadeAlpha = 0.0f;
levelFadeElapsed = 0.0f;
cachedLevel = bgLevel;
printf("Loaded level background for level %d\n", bgLevel);
} else {
printf("Warning: Could not load level background for level %d\n", bgLevel);
cachedLevel = -1; // Mark as failed
// don't change textures if file missing
cachedLevel = -1;
}
}
// Draw cached level background if available
if (levelBackgroundTex) {
// Stretch background to full viewport
// Draw blended backgrounds if needed
if (levelBackgroundTex || nextLevelBackgroundTex) {
SDL_FRect fullRect = { 0, 0, (float)logicalVP.w, (float)logicalVP.h };
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
// if fade in progress
if (nextLevelBackgroundTex && levelFadeAlpha < 1.0f && levelBackgroundTex) {
// draw current with inverse alpha
SDL_SetTextureAlphaMod(levelBackgroundTex, Uint8((1.0f - levelFadeAlpha) * 255));
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
SDL_SetTextureAlphaMod(nextLevelBackgroundTex, Uint8(levelFadeAlpha * 255));
SDL_RenderTexture(renderer, nextLevelBackgroundTex, nullptr, &fullRect);
// reset mods
SDL_SetTextureAlphaMod(levelBackgroundTex, 255);
SDL_SetTextureAlphaMod(nextLevelBackgroundTex, 255);
}
else if (nextLevelBackgroundTex && (!levelBackgroundTex || levelFadeAlpha >= 1.0f)) {
// finalise swap
if (levelBackgroundTex) { SDL_DestroyTexture(levelBackgroundTex); }
levelBackgroundTex = nextLevelBackgroundTex;
nextLevelBackgroundTex = nullptr;
levelFadeAlpha = 0.0f;
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
}
else if (levelBackgroundTex) {
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
}
}
} else if (state == AppState::Loading) {
// Use 3D starfield for loading screen (full screen)
starfield3D.draw(renderer);
} else if (state == AppState::Menu) {
// Use static background for menu, stretched to window; no starfield on sides
if (backgroundTex) {
SDL_FRect fullRect = { 0, 0, (float)logicalVP.w, (float)logicalVP.h };
SDL_RenderTexture(renderer, backgroundTex, nullptr, &fullRect);
}
} else {
// Use regular starfield for other states (not gameplay)
starfield.draw(renderer);
} SDL_SetRenderViewport(renderer, &logicalVP);
}
SDL_SetRenderViewport(renderer, &logicalVP);
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
switch (state)
@ -1059,11 +1211,11 @@ int main(int, char **)
// Calculate dimensions for perfect centering (like JavaScript version)
const bool isLimitedHeight = LOGICAL_H < 450;
const float logoHeight = logoTex ? (isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f) : 0;
const float loadingTextHeight = 24; // Height of "LOADING" text
const float barHeight = 24; // Loading bar height
const float loadingTextHeight = 20; // Height of "LOADING" text (match JS)
const float barHeight = 20; // Loading bar height (match JS)
const float barPaddingVertical = isLimitedHeight ? 15 : 35;
const float percentTextHeight = 24; // Height of percentage text
const float spacingBetweenElements = isLimitedHeight ? 8 : 20;
const float spacingBetweenElements = isLimitedHeight ? 5 : 15;
// Total content height
const float totalContentHeight = logoHeight +
@ -1080,18 +1232,18 @@ int main(int, char **)
// Draw logo (centered, static like JavaScript version)
if (logoTex)
{
// Original logo_small.bmp dimensions
const int lw = 436, lh = 137;
// Calculate scaling like JavaScript version
const float maxLogoWidth = LOGICAL_W * 0.9f;
// Use the same original large logo dimensions as JS (we used a half-size BMP previously)
const int lw = 872, lh = 273;
// Cap logo width similar to JS UI.MAX_LOGO_WIDTH (600) and available screen space
const float maxLogoWidth = std::min(LOGICAL_W * 0.9f, 600.0f);
const float availableHeight = isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f;
const float availableWidth = maxLogoWidth;
const float scaleFactorWidth = availableWidth / lw;
const float scaleFactorHeight = availableHeight / lh;
const float scaleFactorWidth = availableWidth / static_cast<float>(lw);
const float scaleFactorHeight = availableHeight / static_cast<float>(lh);
const float scaleFactor = std::min(scaleFactorWidth, scaleFactorHeight);
const float displayWidth = lw * scaleFactor;
const float displayHeight = lh * scaleFactor;
const float logoX = (LOGICAL_W - displayWidth) / 2.0f;
@ -1106,12 +1258,12 @@ int main(int, char **)
const char* loadingText = "LOADING";
float textWidth = strlen(loadingText) * 12.0f; // Approximate width for pixel font
float textX = (LOGICAL_W - textWidth) / 2.0f;
pixelFont.draw(renderer, textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.5f, {255, 204, 0, 255});
pixelFont.draw(renderer, textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255, 204, 0, 255});
currentY += loadingTextHeight + barPaddingVertical;
// Draw loading bar (like JavaScript version)
const int barW = 300, barH = 24;
const int barW = 400, barH = 20;
const int bx = (LOGICAL_W - barW) / 2;
// Bar border (dark gray) - using drawRect which adds content offset
@ -1136,138 +1288,9 @@ int main(int, char **)
}
break;
case AppState::Menu:
{
// Calculate actual content area (centered within the window)
float contentScale = logicalScale;
float contentW = LOGICAL_W * contentScale;
float contentH = LOGICAL_H * contentScale;
float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
// Draw background if available
if (backgroundTex) {
SDL_FRect bgRect{contentOffsetX, contentOffsetY, LOGICAL_W, LOGICAL_H};
SDL_RenderTexture(renderer, backgroundTex, nullptr, &bgRect);
}
// Draw the enhanced intro screen with logo animation and fireworks
auto drawRect = [&](float x, float y, float w, float h, SDL_Color c)
{ SDL_SetRenderDrawColor(renderer,c.r,c.g,c.b,c.a); SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h}; SDL_RenderFillRect(renderer,&fr); };
// Draw animated logo with sine wave slicing effect (like JS version)
if (logoTex)
{
// logo_small.bmp dimensions
int lw = 436, lh = 137;
int dw = int(LOGICAL_W * 0.5f); // Appropriate size for small logo
int dh = dw * lh / lw;
float logoX = (LOGICAL_W - dw) / 2.f + contentOffsetX;
float logoY = LOGICAL_H * 0.08f + contentOffsetY; // Higher position like JS
// Animate logo with sine wave slices (port from JS version)
for (int slice = 0; slice < dw; slice += 4) {
float offsetY = sin(logoAnimCounter + slice * 0.01f) * 8.0f;
SDL_FRect srcRect = {float(slice * lw / dw), 0, float(4 * lw / dw), float(lh)};
SDL_FRect dstRect = {logoX + slice, logoY + offsetY, 4, float(dh)};
SDL_RenderTexture(renderer, logoTex, &srcRect, &dstRect);
}
}
else
{
font.draw(renderer, LOGICAL_W * 0.5f - 180 + contentOffsetX, LOGICAL_H * 0.1f + contentOffsetY, "TETRIS", 4.0f, SDL_Color{180, 200, 255, 255});
}
// Cloud high scores badge (like JS version)
float badgeWidth = 170;
float badgeHeight = 30;
float badgeX = LOGICAL_W - badgeWidth - 10 + contentOffsetX;
float badgeY = 10 + contentOffsetY;
drawRect(badgeX - contentOffsetX, badgeY - contentOffsetY, badgeWidth, badgeHeight, {0, 0, 0, 178}); // Semi-transparent background
font.draw(renderer, badgeX + 5, badgeY + 8, "CLOUD HIGH SCORES", 1.0f, {66, 133, 244, 255}); // Google blue
// "TOP PLAYERS" section (positioned like JS version)
float topPlayersY = LOGICAL_H * 0.28f + contentOffsetY;
font.draw(renderer, LOGICAL_W * 0.5f - 120 + contentOffsetX, topPlayersY, "TOP PLAYERS", 2.5f, SDL_Color{255, 220, 0, 255});
// High scores table with proper columns (like JS version)
float scoresStartY = topPlayersY + 60;
const auto &hs = scores.all();
// Column headers
float headerY = scoresStartY - 30;
font.draw(renderer, 40 + contentOffsetX, headerY, "RANK", 1.0f, SDL_Color{255, 204, 0, 255});
font.draw(renderer, 120 + contentOffsetX, headerY, "PLAYER", 1.0f, SDL_Color{255, 204, 0, 255});
font.draw(renderer, 280 + contentOffsetX, headerY, "SCORE", 1.0f, SDL_Color{255, 204, 0, 255});
font.draw(renderer, 400 + contentOffsetX, headerY, "LINES", 1.0f, SDL_Color{255, 204, 0, 255});
font.draw(renderer, 500 + contentOffsetX, headerY, "LEVEL", 1.0f, SDL_Color{255, 204, 0, 255});
font.draw(renderer, 600 + contentOffsetX, headerY, "TIME", 1.0f, SDL_Color{255, 204, 0, 255});
// Display high scores (limit to 12 like JS version)
size_t maxDisplay = std::min(hs.size(), size_t(12));
for (size_t i = 0; i < maxDisplay; ++i)
{
float y = scoresStartY + i * 25;
// Rank
char rankStr[8];
snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1);
font.draw(renderer, 40 + contentOffsetX, y, rankStr, 1.1f, SDL_Color{220, 220, 230, 255});
// Player name
font.draw(renderer, 120 + contentOffsetX, y, hs[i].name, 1.1f, SDL_Color{220, 220, 230, 255});
// Score
char scoreStr[16];
snprintf(scoreStr, sizeof(scoreStr), "%d", hs[i].score);
font.draw(renderer, 280 + contentOffsetX, y, scoreStr, 1.1f, SDL_Color{220, 220, 230, 255});
// Lines
char linesStr[8];
snprintf(linesStr, sizeof(linesStr), "%d", hs[i].lines);
font.draw(renderer, 400 + contentOffsetX, y, linesStr, 1.1f, SDL_Color{220, 220, 230, 255});
// Level
char levelStr[8];
snprintf(levelStr, sizeof(levelStr), "%d", hs[i].level);
font.draw(renderer, 500 + contentOffsetX, y, levelStr, 1.1f, SDL_Color{220, 220, 230, 255});
// Time
char timeStr[16];
int mins = int(hs[i].timeSec) / 60;
int secs = int(hs[i].timeSec) % 60;
snprintf(timeStr, sizeof(timeStr), "%d:%02d", mins, secs);
font.draw(renderer, 600 + contentOffsetX, y, timeStr, 1.1f, SDL_Color{220, 220, 230, 255});
}
// Action buttons at bottom (like JS version)
float buttonY = LOGICAL_H * 0.75f + contentOffsetY;
char levelBtnText[32];
snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevelSelection);
drawEnhancedButton(renderer, font, LOGICAL_W * 0.5f + contentOffsetX, buttonY, 200, 50, "PLAY", hoveredButton == 0);
drawEnhancedButton(renderer, font, LOGICAL_W * 0.5f + contentOffsetX, buttonY + 60, 200, 50, levelBtnText, hoveredButton == 1);
// Settings icon/button (top right)
font.draw(renderer, LOGICAL_W - 50, 20, "", 1.5f, SDL_Color{200, 200, 220, 255});
// Draw fireworks animation
drawFireworks(renderer);
// Level selection popup
if (showLevelPopup) {
drawLevelSelectionPopup(renderer, font, startLevelSelection);
}
// Settings popup
if (showSettingsPopup) {
drawSettingsPopup(renderer, font, musicEnabled);
}
// Footer instructions
font.draw(renderer, 20, LOGICAL_H - 30, "F11=FULLSCREEN • L=LEVEL • S=SETTINGS • SPACE=PLAY • M=MUSIC", 1.0f, SDL_Color{150, 150, 170, 255});
}
break;
// Delegate full menu rendering to MenuState object now
menuState->render(renderer, logicalScale, logicalVP);
break;
case AppState::LevelSelect:
font.draw(renderer, LOGICAL_W * 0.5f - 120, 80, "SELECT LEVEL", 2.5f, SDL_Color{255, 220, 0, 255});
{
@ -1577,6 +1600,15 @@ int main(int, char **)
snprintf(timeStr, sizeof(timeStr), "%02d:%02d", mins, secs);
pixelFont.draw(renderer, scoreX, baseY + 290, timeStr, 0.9f, {255, 255, 255, 255});
// --- Gravity HUD: show current gravity in ms and equivalent fps (top-right) ---
{
char gms[64];
double gms_val = game.getGravityMs();
double gfps = gms_val > 0.0 ? (1000.0 / gms_val) : 0.0;
snprintf(gms, sizeof(gms), "GRAV: %.0f ms (%.2f fps)", gms_val, gfps);
pixelFont.draw(renderer, LOGICAL_W - 260, 10, gms, 0.9f, {200, 200, 220, 255});
}
// Hold piece (if implemented)
if (game.held().type < PIECE_COUNT) {
pixelFont.draw(renderer, statsX + 10, statsY + statsH - 80, "HOLD", 1.0f, {255, 220, 0, 255});

View File

@ -4,6 +4,32 @@
#include "../Font.h"
#include <SDL3/SDL.h>
#include <cstdio>
#include <algorithm>
#include <cmath>
// Local logical canvas size (matches main.cpp). Kept local to avoid changing many files.
static constexpr int LOGICAL_W = 1200;
static constexpr int LOGICAL_H = 1000;
extern bool showLevelPopup; // from main
extern bool showSettingsPopup; // from main
extern bool musicEnabled; // from main
extern int hoveredButton; // from main
// Call wrappers defined in main.cpp
extern void menu_drawFireworks(SDL_Renderer* renderer);
extern void menu_updateFireworks(double frameMs);
extern double menu_getLogoAnimCounter();
extern int menu_getHoveredButton();
extern void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, bool isHovered, bool isSelected);
// Menu button wrapper implemented in main.cpp
extern void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, SDL_Color bgColor, SDL_Color borderColor);
// wrappers for popups (defined in main.cpp)
extern void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel);
extern void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
@ -15,19 +41,125 @@ void MenuState::onExit() {
}
void MenuState::handleEvent(const SDL_Event& e) {
// Menu-specific key handling moved from main; main still handles mouse for now
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
if (ctx.startLevelSelection && *ctx.startLevelSelection >= 0) {
// keep simple: allow L/S toggles handled globally in main for now
}
}
// Key-specific handling (allow main to handle global keys)
(void)e;
}
void MenuState::update(double frameMs) {
// Update logo animation counter and particles similar to main
(void)frameMs;
}
void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
(void)renderer; (void)logicalScale; (void)logicalVP;
// Main still performs actual rendering for now; this placeholder keeps the API.
// Compute content offset using the same math as main
float winW = float(logicalVP.w);
float winH = float(logicalVP.h);
float contentScale = logicalScale;
float contentW = LOGICAL_W * contentScale;
float contentH = LOGICAL_H * contentScale;
float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
// Background is drawn by main (stretched to the full window) to avoid double-draw.
// Draw the animated logo and fireworks using the small logo if available (show whole image)
SDL_Texture* logoToUse = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex;
if (logoToUse) {
// Use dimensions provided by the shared context when available
int texW = (logoToUse == ctx.logoSmallTex && ctx.logoSmallW > 0) ? ctx.logoSmallW : 872;
int texH = (logoToUse == ctx.logoSmallTex && ctx.logoSmallH > 0) ? ctx.logoSmallH : 273;
float maxW = LOGICAL_W * 0.6f;
float scale = std::min(1.0f, maxW / float(texW));
float dw = texW * scale;
float dh = texH * scale;
float logoX = (LOGICAL_W - dw) / 2.f + contentOffsetX;
float logoY = LOGICAL_H * 0.05f + contentOffsetY;
SDL_FRect dst{logoX, logoY, dw, dh};
SDL_RenderTexture(renderer, logoToUse, nullptr, &dst);
}
// Fireworks (draw above high scores / near buttons)
menu_drawFireworks(renderer);
// Score list and top players with a sine-wave vertical animation (use pixelFont for retro look)
float topPlayersY = LOGICAL_H * 0.30f + contentOffsetY; // more top padding
FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
if (useFont) {
useFont->draw(renderer, LOGICAL_W * 0.5f - 110 + contentOffsetX, topPlayersY, std::string("TOP PLAYERS"), 1.8f, SDL_Color{255, 220, 0, 255});
}
// High scores table with wave offset
float scoresStartY = topPlayersY + 70; // more spacing under title
const auto &hs = ctx.scores ? ctx.scores->all() : *(new std::vector<ScoreEntry>());
size_t maxDisplay = std::min(hs.size(), size_t(12));
// Draw table header
if (useFont) {
float cx = LOGICAL_W * 0.5f + contentOffsetX;
float colX[] = { cx - 280, cx - 180, cx - 20, cx + 90, cx + 200, cx + 300 };
useFont->draw(renderer, colX[0], scoresStartY - 28, "RANK", 1.1f, SDL_Color{200,200,220,255});
useFont->draw(renderer, colX[1], scoresStartY - 28, "PLAYER", 1.1f, SDL_Color{200,200,220,255});
useFont->draw(renderer, colX[2], scoresStartY - 28, "SCORE", 1.1f, SDL_Color{200,200,220,255});
useFont->draw(renderer, colX[3], scoresStartY - 28, "LINES", 1.1f, SDL_Color{200,200,220,255});
useFont->draw(renderer, colX[4], scoresStartY - 28, "LEVEL", 1.1f, SDL_Color{200,200,220,255});
useFont->draw(renderer, colX[5], scoresStartY - 28, "TIME", 1.1f, SDL_Color{200,200,220,255});
}
for (size_t i = 0; i < maxDisplay; ++i)
{
float baseY = scoresStartY + i * 25;
float wave = std::sin((float)menu_getLogoAnimCounter() * 0.006f + i * 0.25f) * 6.0f; // subtle wave
float y = baseY + wave;
// Center columns around mid X, wider
float cx = LOGICAL_W * 0.5f + contentOffsetX;
float colX[] = { cx - 280, cx - 180, cx - 20, cx + 90, cx + 200, cx + 300 };
char rankStr[8];
std::snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1);
if (useFont) useFont->draw(renderer, colX[0], y, rankStr, 1.0f, SDL_Color{220, 220, 230, 255});
if (useFont) useFont->draw(renderer, colX[1], y, hs[i].name, 1.0f, SDL_Color{220, 220, 230, 255});
char scoreStr[16]; std::snprintf(scoreStr, sizeof(scoreStr), "%d", hs[i].score);
if (useFont) useFont->draw(renderer, colX[2], y, scoreStr, 1.0f, SDL_Color{220, 220, 230, 255});
char linesStr[8]; std::snprintf(linesStr, sizeof(linesStr), "%d", hs[i].lines);
if (useFont) useFont->draw(renderer, colX[3], y, linesStr, 1.0f, SDL_Color{220, 220, 230, 255});
char levelStr[8]; std::snprintf(levelStr, sizeof(levelStr), "%d", hs[i].level);
if (useFont) useFont->draw(renderer, colX[4], y, levelStr, 1.0f, SDL_Color{220, 220, 230, 255});
char timeStr[16]; int mins = int(hs[i].timeSec) / 60; int secs = int(hs[i].timeSec) % 60;
std::snprintf(timeStr, sizeof(timeStr), "%d:%02d", mins, secs);
if (useFont) useFont->draw(renderer, colX[5], y, timeStr, 1.0f, SDL_Color{220, 220, 230, 255});
}
// Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test)
bool isSmall = (contentW < 700.0f);
float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f;
float btnH = isSmall ? 60.0f : 70.0f;
float btnX = LOGICAL_W * 0.5f + contentOffsetX;
// Move buttons down by 40px to match original layout (user requested 30-50px)
const float btnYOffset = 40.0f;
float btnY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; // align with main's button vertical position
if (ctx.pixelFont) {
char levelBtnText[32];
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
// left = green, right = blue like original
menu_drawMenuButton(renderer, *ctx.pixelFont, btnX - btnW * 0.6f, btnY, btnW, btnH, std::string("PLAY"), SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255});
menu_drawMenuButton(renderer, *ctx.pixelFont, btnX + btnW * 0.6f, btnY, btnW, btnH, std::string(levelBtnText), SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255});
}
// Popups (level/settings) if requested
if (ctx.showLevelPopup && *ctx.showLevelPopup) {
// call wrapper which will internally draw on top of current content
// prefer pixelFont for retro look
FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
menu_drawLevelSelectionPopup(renderer, *useFont, ctx.backgroundTex, ctx.startLevelSelection ? *ctx.startLevelSelection : 0);
}
if (ctx.showSettingsPopup && *ctx.showSettingsPopup) {
menu_drawSettingsPopup(renderer, *ctx.font, ctx.musicEnabled ? *ctx.musicEnabled : false);
}
}

View File

@ -0,0 +1,52 @@
#include "PlayingState.h"
#include "../Game.h"
#include "../LineEffect.h"
#include "../Scores.h"
#include <SDL3/SDL.h>
PlayingState::PlayingState(StateContext& ctx) : State(ctx) {}
void PlayingState::onEnter() {
// Nothing yet; main still owns game creation
}
void PlayingState::onExit() {
}
void PlayingState::handleEvent(const SDL_Event& e) {
// We keep short-circuited input here; main still handles mouse UI
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
if (!ctx.game) return;
// Pause toggle (P)
if (e.key.scancode == SDL_SCANCODE_P) {
bool paused = ctx.game->isPaused();
ctx.game->setPaused(!paused);
}
// Other gameplay keys already registered by main's Playing handler for now
}
}
void PlayingState::update(double frameMs) {
if (!ctx.game) return;
// forward per-frame gameplay updates (gravity, elapsed)
if (!ctx.game->isPaused()) {
ctx.game->tickGravity(frameMs);
ctx.game->addElapsed(frameMs);
if (ctx.lineEffect && ctx.lineEffect->isActive()) {
if (ctx.lineEffect->update(frameMs / 1000.0f)) {
ctx.game->clearCompletedLines();
}
}
}
if (ctx.game->isGameOver()) {
if (ctx.scores) ctx.scores->submit(ctx.game->score(), ctx.game->lines(), ctx.game->level(), ctx.game->elapsed());
// Transitioning state must be done by the owner (main via StateManager hooks). We can't set state here.
}
}
void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
if (!ctx.game) return;
// Rendering kept in main for now to avoid changing many layout calculations in one change.
}

17
src/states/PlayingState.h Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include "State.h"
#include <SDL3/SDL.h>
class PlayingState : public State {
public:
PlayingState(StateContext& ctx);
void onEnter() override;
void onExit() override;
void handleEvent(const SDL_Event& e) override;
void update(double frameMs) override;
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
private:
// Local per-state variables if needed
bool localPaused = false;
};

View File

@ -25,6 +25,9 @@ struct StateContext {
// Textures
SDL_Texture* logoTex = nullptr;
SDL_Texture* logoSmallTex = nullptr;
int logoSmallW = 0;
int logoSmallH = 0;
SDL_Texture* backgroundTex = nullptr;
SDL_Texture* blocksTex = nullptr;
@ -33,6 +36,9 @@ struct StateContext {
bool* musicEnabled = nullptr;
int* startLevelSelection = nullptr;
int* hoveredButton = nullptr;
// Menu popups (exposed from main)
bool* showLevelPopup = nullptr;
bool* showSettingsPopup = nullptr;
};
class State {