diff --git a/CMakeLists.txt b/CMakeLists.txt index 41639dd..831fb0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,8 +33,39 @@ add_executable(tetris # State implementations (new) src/states/LoadingState.cpp src/states/MenuState.cpp + src/states/PlayingState.cpp ) +if (WIN32) + # Embed the application icon into the executable + set_source_files_properties(src/app_icon.rc PROPERTIES LANGUAGE RC) + target_sources(tetris PRIVATE src/app_icon.rc) +endif() + +if (WIN32) + # Ensure favicon.ico is available in the build directory for the resource compiler + set(FAVICON_SRC "${CMAKE_SOURCE_DIR}/assets/favicon/favicon.ico") + set(FAVICON_DEST "${CMAKE_BINARY_DIR}/favicon.ico") + if(EXISTS ${FAVICON_SRC}) + add_custom_command( + OUTPUT ${FAVICON_DEST} + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${FAVICON_SRC} ${FAVICON_DEST} + DEPENDS ${FAVICON_SRC} + COMMENT "Copy favicon.ico to build dir for resource compilation" + ) + add_custom_target(copy_favicon ALL DEPENDS ${FAVICON_DEST}) + add_dependencies(tetris copy_favicon) + else() + message(WARNING "Favicon not found at ${FAVICON_SRC}; app icon may not compile") + endif() + + # Also copy favicon into the runtime output folder (same dir as exe) + add_custom_command(TARGET tetris POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${FAVICON_SRC} $/favicon.ico + COMMENT "Copy favicon.ico next to executable" + ) +endif() + target_link_libraries(tetris PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf) if (WIN32) diff --git a/docs/DEPENDENCY_OPTIMIZATION.md b/DEPENDENCY_OPTIMIZATION.md similarity index 100% rename from docs/DEPENDENCY_OPTIMIZATION.md rename to DEPENDENCY_OPTIMIZATION.md diff --git a/docs/DEPLOYMENT.md b/DEPLOYMENT.md similarity index 100% rename from docs/DEPLOYMENT.md rename to DEPLOYMENT.md diff --git a/FreeSans.ttf b/FreeSans.ttf new file mode 100644 index 0000000..9db9585 Binary files /dev/null and b/FreeSans.ttf differ diff --git a/docs/I_PIECE_SPAWN_FIX.md b/I_PIECE_SPAWN_FIX.md similarity index 100% rename from docs/I_PIECE_SPAWN_FIX.md rename to I_PIECE_SPAWN_FIX.md diff --git a/docs/LAYOUT_IMPROVEMENTS.md b/LAYOUT_IMPROVEMENTS.md similarity index 100% rename from docs/LAYOUT_IMPROVEMENTS.md rename to LAYOUT_IMPROVEMENTS.md diff --git a/docs/LOADING_PROGRESS_FIX.md b/LOADING_PROGRESS_FIX.md similarity index 100% rename from docs/LOADING_PROGRESS_FIX.md rename to LOADING_PROGRESS_FIX.md diff --git a/docs/SOUND_EFFECTS_IMPLEMENTATION.md b/SOUND_EFFECTS_IMPLEMENTATION.md similarity index 100% rename from docs/SOUND_EFFECTS_IMPLEMENTATION.md rename to SOUND_EFFECTS_IMPLEMENTATION.md diff --git a/docs/SPAWN_AND_FONT_IMPROVEMENTS.md b/SPAWN_AND_FONT_IMPROVEMENTS.md similarity index 100% rename from docs/SPAWN_AND_FONT_IMPROVEMENTS.md rename to SPAWN_AND_FONT_IMPROVEMENTS.md diff --git a/docs/SPAWN_AND_GRID_FIXES.md b/SPAWN_AND_GRID_FIXES.md similarity index 100% rename from docs/SPAWN_AND_GRID_FIXES.md rename to SPAWN_AND_GRID_FIXES.md diff --git a/src/Game.cpp b/src/Game.cpp index 6e95ab4..670bd55 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -14,12 +14,45 @@ static const std::array 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 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) { diff --git a/src/Game.h b/src/Game.h index cd92248..16218b3 100644 --- a/src/Game.h +++ b/src/Game.h @@ -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 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) }; diff --git a/src/LineEffect.cpp b/src/LineEffect.cpp index 73c00fd..ffeec75 100644 --- a/src/LineEffect.cpp +++ b/src/LineEffect.cpp @@ -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()) { diff --git a/src/SoundEffect.cpp b/src/SoundEffect.cpp index 5826d07..412af64 100644 --- a/src/SoundEffect.cpp +++ b/src/SoundEffect.cpp @@ -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& 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; } diff --git a/src/app_icon.rc b/src/app_icon.rc new file mode 100644 index 0000000..eb2ef6f --- /dev/null +++ b/src/app_icon.rc @@ -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" diff --git a/src/main.cpp b/src/main.cpp index 86b9462..d892f38 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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> 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 particles; - + std::vector 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(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(ctx); auto menuState = std::make_unique(ctx); + auto playingState = std::make_unique(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(lw); + const float scaleFactorHeight = availableHeight / static_cast(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}); diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 2d3f837..350b959 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -4,6 +4,32 @@ #include "../Font.h" #include #include +#include +#include + +// 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()); + 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); + } } diff --git a/src/states/PlayingState.cpp b/src/states/PlayingState.cpp new file mode 100644 index 0000000..58dea7e --- /dev/null +++ b/src/states/PlayingState.cpp @@ -0,0 +1,52 @@ +#include "PlayingState.h" +#include "../Game.h" +#include "../LineEffect.h" +#include "../Scores.h" +#include + +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. +} diff --git a/src/states/PlayingState.h b/src/states/PlayingState.h new file mode 100644 index 0000000..f8b177e --- /dev/null +++ b/src/states/PlayingState.h @@ -0,0 +1,17 @@ +#pragma once +#include "State.h" +#include + +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; +}; diff --git a/src/states/State.h b/src/states/State.h index f1f292c..245d9f7 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -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 {