diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c8044c..c77e335 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ set(TETRIS_SOURCES src/persistence/Scores.cpp src/graphics/effects/Starfield.cpp src/graphics/effects/Starfield3D.cpp + src/graphics/effects/SpaceWarp.cpp src/graphics/ui/Font.cpp src/graphics/ui/HelpOverlay.cpp src/graphics/renderers/GameRenderer.cpp diff --git a/assets/images/main_screen.bmp b/assets/images/main_screen.bmp new file mode 100644 index 0000000..e2b39b7 Binary files /dev/null and b/assets/images/main_screen.bmp differ diff --git a/assets/images/main_screen_001.png b/assets/images/main_screen_001.png new file mode 100644 index 0000000..5aaa681 Binary files /dev/null and b/assets/images/main_screen_001.png differ diff --git a/assets/images/main_screen_002.png b/assets/images/main_screen_002.png new file mode 100644 index 0000000..f54a8e1 Binary files /dev/null and b/assets/images/main_screen_002.png differ diff --git a/assets/images/main_screen_003.png b/assets/images/main_screen_003.png new file mode 100644 index 0000000..9e6793f Binary files /dev/null and b/assets/images/main_screen_003.png differ diff --git a/assets/images/main_screen_004.png b/assets/images/main_screen_004.png new file mode 100644 index 0000000..f884159 Binary files /dev/null and b/assets/images/main_screen_004.png differ diff --git a/src/graphics/effects/SpaceWarp.cpp b/src/graphics/effects/SpaceWarp.cpp new file mode 100644 index 0000000..4acf0ed --- /dev/null +++ b/src/graphics/effects/SpaceWarp.cpp @@ -0,0 +1,149 @@ +#include "SpaceWarp.h" + +#include +#include +#include + +namespace { +constexpr float MIN_ASPECT = 0.001f; +} + +SpaceWarp::SpaceWarp() { + std::random_device rd; + rng.seed(rd()); +} + +void SpaceWarp::init(int w, int h, int starCount) { + resize(w, h); + stars.resize(std::max(8, starCount)); + for (auto& star : stars) { + respawn(star, true); + } +} + +void SpaceWarp::resize(int w, int h) { + width = std::max(1, w); + height = std::max(1, h); + centerX = width * 0.5f; + centerY = height * 0.5f; + warpFactor = std::max(width, height) * settings.warpFactorScale; +} + +void SpaceWarp::setSettings(const SpaceWarpSettings& newSettings) { + settings = newSettings; + warpFactor = std::max(width, height) * settings.warpFactorScale; +} + +float SpaceWarp::randomRange(float min, float max) { + std::uniform_real_distribution dist(min, max); + return dist(rng); +} + +static int randomIntInclusive(std::mt19937& rng, int min, int max) { + std::uniform_int_distribution dist(min, max); + return dist(rng); +} + +void SpaceWarp::respawn(WarpStar& star, bool randomDepth) { + float aspect = static_cast(width) / static_cast(std::max(1, height)); + float normalizedAspect = std::max(aspect, MIN_ASPECT); + float xRange = settings.baseSpawnRange * (aspect >= 1.0f ? aspect : 1.0f); + float yRange = settings.baseSpawnRange * (aspect >= 1.0f ? 1.0f : (1.0f / normalizedAspect)); + star.x = randomRange(-xRange, xRange); + star.y = randomRange(-yRange, yRange); + star.z = randomDepth ? randomRange(minDepth, maxDepth) : maxDepth; + star.speed = randomRange(settings.minSpeed, settings.maxSpeed); + star.shade = randomRange(settings.minShade, settings.maxShade); + static constexpr Uint8 GRAY_SHADES[] = {160, 180, 200, 220, 240}; + int idx = randomIntInclusive(rng, 0, int(std::size(GRAY_SHADES)) - 1); + star.baseShade = GRAY_SHADES[idx]; + star.prevScreenX = centerX; + star.prevScreenY = centerY; + star.screenX = centerX; + star.screenY = centerY; +} + +bool SpaceWarp::project(const WarpStar& star, float& outX, float& outY) const { + if (star.z <= minDepth) { + return false; + } + float perspective = warpFactor / (star.z + 0.001f); + outX = centerX + star.x * perspective; + outY = centerY + star.y * perspective; + const float margin = settings.spawnMargin; + return outX >= -margin && outX <= width + margin && outY >= -margin && outY <= height + margin; +} + +void SpaceWarp::update(float deltaSeconds) { + if (stars.empty()) { + return; + } + + for (auto& star : stars) { + star.z -= star.speed * deltaSeconds; + if (star.z <= minDepth) { + respawn(star, true); + continue; + } + + float sx = 0.0f; + float sy = 0.0f; + if (!project(star, sx, sy)) { + respawn(star, true); + continue; + } + + star.prevScreenX = star.screenX; + star.prevScreenY = star.screenY; + star.screenX = sx; + star.screenY = sy; + + float dx = star.screenX - star.prevScreenX; + float dy = star.screenY - star.prevScreenY; + float lenSq = dx * dx + dy * dy; + float maxStreak = std::max(settings.maxTrailLength, 0.0f); + if (maxStreak > 0.0f && lenSq > maxStreak * maxStreak) { + float len = std::sqrt(lenSq); + float scale = maxStreak / len; + star.prevScreenX = star.screenX - dx * scale; + star.prevScreenY = star.screenY - dy * scale; + } + } +} + +void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) { + if (stars.empty()) { + return; + } + + SDL_BlendMode previous = SDL_BLENDMODE_NONE; + SDL_GetRenderDrawBlendMode(renderer, &previous); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); + + for (const auto& star : stars) { + float depthFactor = 1.0f - std::clamp(star.z / maxDepth, 0.0f, 1.0f); + float alphaBase = std::clamp(settings.minAlpha + depthFactor * settings.alphaDepthBoost, 0.0f, 255.0f); + Uint8 alpha = static_cast(std::clamp(alphaBase * alphaScale, 0.0f, 255.0f)); + float colorValue = std::clamp( + star.baseShade * (settings.baseShadeScale + depthFactor * settings.depthColorScale) * star.shade, + settings.minColor, + settings.maxColor); + Uint8 color = static_cast(colorValue); + + if (settings.drawTrails) { + float trailAlphaFloat = alpha * settings.trailAlphaScale; + Uint8 trailAlpha = static_cast(std::clamp(trailAlphaFloat, 0.0f, 255.0f)); + SDL_SetRenderDrawColor(renderer, color, color, color, trailAlpha); + SDL_RenderLine(renderer, star.prevScreenX, star.prevScreenY, star.screenX, star.screenY); + } + + float dotSize = std::clamp(settings.minDotSize + depthFactor * (settings.maxDotSize - settings.minDotSize), + settings.minDotSize, + settings.maxDotSize); + SDL_FRect dot{star.screenX - dotSize * 0.5f, star.screenY - dotSize * 0.5f, dotSize, dotSize}; + SDL_SetRenderDrawColor(renderer, color, color, color, alpha); + SDL_RenderFillRect(renderer, &dot); + } + + SDL_SetRenderDrawBlendMode(renderer, previous); +} diff --git a/src/graphics/effects/SpaceWarp.h b/src/graphics/effects/SpaceWarp.h new file mode 100644 index 0000000..1a51d9c --- /dev/null +++ b/src/graphics/effects/SpaceWarp.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include + +struct SpaceWarpSettings { + float baseSpawnRange = 1.45f; // logical radius for initial star positions + float warpFactorScale = 122.85f; // scales perspective factor so stars spread faster or slower + float spawnMargin = 60.0f; // how far offscreen a star can travel before respawn + float minShade = 0.85f; // lower bound for per-star brightness multiplier + float maxShade = 1.15f; // upper bound for per-star brightness multiplier + float minSpeed = 120.0f; // slowest warp velocity (higher feels faster motion) + float maxSpeed = 280.0f; // fastest warp velocity + float minDotSize = 2.5f; // smallest star size in pixels + float maxDotSize = 4.5f; // largest star size in pixels + float minAlpha = 70.0f; // base opacity even for distant stars + float alphaDepthBoost = 160.0f; // extra opacity applied as stars approach the camera + float minColor = 180.0f; // clamp for minimum grayscale value + float maxColor = 205.0f; // clamp for maximum grayscale value + float baseShadeScale = 0.75f; // baseline multiplier applied to the sampled grayscale shade + float depthColorScale = 0.55f; // how much depth affects the grayscale brightness + bool drawTrails = true; // when true, also render streak lines for hyper-speed look + float trailAlphaScale = 0.75f; // relative opacity for streak lines vs dots + float maxTrailLength = 36.0f; // clamp length of each streak in pixels +}; + +class SpaceWarp { +public: + SpaceWarp(); + void init(int width, int height, int starCount = 320); + void resize(int width, int height); + void update(float deltaSeconds); + void draw(SDL_Renderer* renderer, float alphaScale = 1.0f); + void setSettings(const SpaceWarpSettings& newSettings); + const SpaceWarpSettings& getSettings() const { return settings; } + +private: + struct WarpStar { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float speed = 0.0f; + float prevScreenX = 0.0f; + float prevScreenY = 0.0f; + float screenX = 0.0f; + float screenY = 0.0f; + float shade = 1.0f; + Uint8 baseShade = 220; + }; + + void respawn(WarpStar& star, bool randomDepth = true); + bool project(const WarpStar& star, float& outX, float& outY) const; + float randomRange(float min, float max); + + std::vector stars; + std::mt19937 rng; + + int width = 0; + int height = 0; + float centerX = 0.0f; + float centerY = 0.0f; + float warpFactor = 520.0f; + + SpaceWarpSettings settings{}; + + float minDepth = 2.0f; + float maxDepth = 320.0f; +}; diff --git a/src/main.cpp b/src/main.cpp index b540136..f1ff782 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,6 +25,7 @@ #include "persistence/Scores.h" #include "graphics/effects/Starfield.h" #include "graphics/effects/Starfield3D.h" +#include "graphics/effects/SpaceWarp.h" #include "graphics/ui/Font.h" #include "graphics/ui/HelpOverlay.h" #include "gameplay/effects/LineEffect.h" @@ -680,16 +681,15 @@ static void drawFireworks_impl(SDL_Renderer* renderer, SDL_Texture*) { SDL_SetRenderDrawBlendMode(renderer, previousBlend); } -// External wrappers for use by other translation units (MenuState) -// Expect callers to pass the blocks texture via StateContext so we avoid globals. -void menu_drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex) { drawFireworks_impl(renderer, blocksTex); } -void menu_updateFireworks(double frameMs) { updateFireworks(frameMs); } +// External wrappers retained for compatibility; now no-ops to disable the legacy fireworks effect. +void menu_drawFireworks(SDL_Renderer*, SDL_Texture*) {} +void menu_updateFireworks(double) {} double menu_getLogoAnimCounter() { return logoAnimCounter; } int menu_getHoveredButton() { return hoveredButton; } int main(int, char **) { - // Initialize random seed for fireworks + // Initialize random seed for procedural effects srand(static_cast(SDL_GetTicks())); // Load settings @@ -779,6 +779,8 @@ int main(int, char **) starfield.init(200, LOGICAL_W, LOGICAL_H); Starfield3D starfield3D; starfield3D.init(LOGICAL_W, LOGICAL_H, 200); + SpaceWarp spaceWarp; + spaceWarp.init(LOGICAL_W, LOGICAL_H, 420); // Initialize line clearing effects LineEffect lineEffect; @@ -794,6 +796,27 @@ int main(int, char **) // Load menu background using SDL_image (prefers JPEG) SDL_Texture* backgroundTex = loadTextureFromImage(renderer, "assets/images/main_background.bmp"); + // Load the new main screen overlay that sits above the background but below buttons + int mainScreenW = 0; + int mainScreenH = 0; + SDL_Texture* mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen_004.png", &mainScreenW, &mainScreenH); + if (mainScreenTex) { + SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded main_screen overlay %dx%d (tex=%p)", mainScreenW, mainScreenH, (void*)mainScreenTex); + FILE* f = fopen("tetris_trace.log", "a"); + if (f) { + fprintf(f, "main.cpp: loaded main_screen.bmp %dx%d tex=%p\n", mainScreenW, mainScreenH, (void*)mainScreenTex); + fclose(f); + } + } else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to load assets/images/main_screen.bmp (overlay will be skipped)"); + FILE* f = fopen("tetris_trace.log", "a"); + if (f) { + fprintf(f, "main.cpp: failed to load main_screen.bmp\n"); + fclose(f); + } + } + // Note: `backgroundTex` is owned by main and passed into `StateContext::backgroundTex` below. // States should render using `ctx.backgroundTex` rather than accessing globals. @@ -971,6 +994,9 @@ int main(int, char **) ctx.logoSmallH = logoSmallH; ctx.backgroundTex = backgroundTex; ctx.blocksTex = blocksTex; + ctx.mainScreenTex = mainScreenTex; + ctx.mainScreenW = mainScreenW; + ctx.mainScreenH = mainScreenH; ctx.musicEnabled = &musicEnabled; ctx.startLevelSelection = &startLevelSelection; ctx.hoveredButton = &hoveredButton; @@ -1565,21 +1591,25 @@ int main(int, char **) } previousState = state; - // Update starfields based on current state + // Update background effects if (state == AppState::Loading) { starfield3D.update(float(frameMs / 1000.0f)); - starfield3D.resize(logicalVP.w, logicalVP.h); // Update for window resize + starfield3D.resize(winW, winH); } else { starfield.update(float(frameMs / 1000.0f), logicalVP.x * 2 + logicalVP.w, logicalVP.y * 2 + logicalVP.h); } + if (state == AppState::Menu) { + spaceWarp.resize(winW, winH); + spaceWarp.update(float(frameMs / 1000.0f)); + } + // Advance level background fade if a next texture is queued updateLevelBackgroundFade(levelBackgrounds, float(frameMs)); // Update intro animations if (state == AppState::Menu) { logoAnimCounter += frameMs * 0.0008; // Animation speed - updateFireworks(frameMs); } // --- Per-state update hooks (allow states to manage logic incrementally) @@ -1671,7 +1701,7 @@ int main(int, char **) // --- Render --- SDL_SetRenderViewport(renderer, nullptr); - SDL_SetRenderDrawColor(renderer, 12, 12, 16, 255); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderClear(renderer); // Draw level-based background for gameplay, starfield for other states @@ -1682,8 +1712,32 @@ int main(int, char **) } else if (state == AppState::Loading) { // Use 3D starfield for loading screen (full screen) starfield3D.draw(renderer); - } else if (state == AppState::Menu || state == AppState::LevelSelector || state == AppState::Options) { - // Use static background for menu, stretched to window; no starfield on sides + } else if (state == AppState::Menu) { + // Space flyover backdrop for the main screen + spaceWarp.draw(renderer, 1.0f); + + if (mainScreenTex) { + float texW = mainScreenW > 0 ? static_cast(mainScreenW) : 0.0f; + float texH = mainScreenH > 0 ? static_cast(mainScreenH) : 0.0f; + if (texW <= 0.0f || texH <= 0.0f) { + if (!SDL_GetTextureSize(mainScreenTex, &texW, &texH)) { + texW = texH = 0.0f; + } + } + if (texW > 0.0f && texH > 0.0f) { + const float drawH = static_cast(winH); + const float scale = drawH / texH; + const float drawW = texW * scale; + SDL_FRect dst{ + (winW - drawW) * 0.5f, + 0.0f, + drawW, + drawH + }; + SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst); + } + } + } else if (state == AppState::LevelSelector || state == AppState::Options) { if (backgroundTex) { SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH }; SDL_RenderTexture(renderer, backgroundTex, nullptr, &fullRect); @@ -2021,6 +2075,8 @@ int main(int, char **) SDL_DestroyTexture(logoTex); if (backgroundTex) SDL_DestroyTexture(backgroundTex); + if (mainScreenTex) + SDL_DestroyTexture(mainScreenTex); resetLevelBackgrounds(levelBackgrounds); if (blocksTex) SDL_DestroyTexture(blocksTex); diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 11a6adc..ed3c5f9 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -19,6 +19,8 @@ // `ctx.musicEnabled` and `ctx.hoveredButton` instead to avoid globals. // Menu helper wrappers are declared in a shared header implemented in main.cpp #include "../audio/MenuWrappers.h" +#include "../utils/ImagePathResolver.h" +#include MenuState::MenuState(StateContext& ctx) : State(ctx) {} @@ -159,9 +161,6 @@ void MenuState::handleEvent(const SDL_Event& e) { void MenuState::update(double frameMs) { // Update logo animation counter GlobalState::instance().logoAnimCounter += frameMs; - - // Update fireworks particles - GlobalState::instance().updateFireworks(frameMs); } void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { @@ -184,67 +183,32 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; // 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; - // Trace logo texture pointer { - FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render logoToUse=%llu\n", (unsigned long long)(uintptr_t)logoToUse); fclose(f); } - } - 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}; - // Trace before rendering logo - { - FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render before SDL_RenderTexture logo ptr=%llu\n", (unsigned long long)(uintptr_t)logoToUse); fclose(f); } - } - SDL_RenderTexture(renderer, logoToUse, nullptr, &dst); - // Trace after rendering logo - { - FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after SDL_RenderTexture logo\n"); fclose(f); } + FILE* f = fopen("tetris_trace.log", "a"); + if (f) { + fprintf(f, "MenuState::render ctx.mainScreenTex=%llu (w=%d h=%d)\n", + (unsigned long long)(uintptr_t)ctx.mainScreenTex, + ctx.mainScreenW, + ctx.mainScreenH); + fclose(f); } } - // Fireworks (draw above high scores / near buttons) - { - FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render before drawFireworks blocksTex=%llu\n", (unsigned long long)(uintptr_t)ctx.blocksTex); fclose(f); } - } - GlobalState::instance().drawFireworks(renderer, ctx.blocksTex); - { - FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after drawFireworks\n"); fclose(f); } - } - - // 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; + float topPlayersY = LOGICAL_H * 0.24f + contentOffsetY; if (useFont) { - { - FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render before useFont->draw TOP PLAYERS ptr=%llu\n", (unsigned long long)(uintptr_t)useFont); fclose(f); } - } const std::string title = "TOP PLAYERS"; - int tW = 0, tH = 0; useFont->measure(title, 1.8f, tW, tH); + int tW = 0, tH = 0; + useFont->measure(title, 1.8f, tW, tH); float titleX = (LOGICAL_W - (float)tW) * 0.5f + contentOffsetX; useFont->draw(renderer, titleX, topPlayersY, title, 1.8f, SDL_Color{255, 220, 0, 255}); - { - FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after useFont->draw TOP PLAYERS\n"); fclose(f); } - } } - // High scores table with wave offset - float scoresStartY = topPlayersY + 70; // more spacing under title + float scoresStartY = topPlayersY + 70.0f; static const std::vector EMPTY_SCORES; const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES; 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 }; @@ -254,36 +218,126 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi 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.0f; + float wave = std::sin((float)GlobalState::instance().logoAnimCounter * 0.006f + i * 0.25f) * 6.0f; + float y = baseY + wave; + + char rankStr[8]; + std::snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1); + useFont->draw(renderer, colX[0], y, rankStr, 1.0f, SDL_Color{220, 220, 230, 255}); + + 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); + 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); + 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); + 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); + useFont->draw(renderer, colX[5], y, timeStr, 1.0f, SDL_Color{220, 220, 230, 255}); + } } - - // 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 }; - - for (size_t i = 0; i < maxDisplay; ++i) - { - float baseY = scoresStartY + i * 25; - float wave = std::sin((float)GlobalState::instance().logoAnimCounter * 0.006f + i * 0.25f) * 6.0f; // subtle wave - float y = baseY + wave; - 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}); + // Draw the sci-fi overlay that sits above the scoreboard but below the buttons + SDL_Texture* overlayTex = ctx.mainScreenTex; + int overlayW = ctx.mainScreenW; + int overlayH = ctx.mainScreenH; - if (useFont) useFont->draw(renderer, colX[1], y, hs[i].name, 1.0f, SDL_Color{220, 220, 230, 255}); + static SDL_Texture* fallbackOverlay = nullptr; + static int fallbackW = 0; + static int fallbackH = 0; - 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}); + if (!overlayTex) { + if (!fallbackOverlay) { + const std::string resolvedOverlay = AssetPath::resolveImagePath("assets/images/main_screen.bmp"); + fallbackOverlay = IMG_LoadTexture(renderer, resolvedOverlay.c_str()); + if (fallbackOverlay) { + SDL_SetTextureBlendMode(fallbackOverlay, SDL_BLENDMODE_BLEND); + float tmpW = 0.0f; + float tmpH = 0.0f; + SDL_GetTextureSize(fallbackOverlay, &tmpW, &tmpH); + fallbackW = static_cast(tmpW); + fallbackH = static_cast(tmpH); + FILE* f = fopen("tetris_trace.log", "a"); + if (f) { + fprintf(f, "MenuState::render loaded fallback overlay texture %p path=%s size=%dx%d\n", + (void*)fallbackOverlay, resolvedOverlay.c_str(), fallbackW, fallbackH); + fclose(f); + } + } else { + FILE* f = fopen("tetris_trace.log", "a"); + if (f) { + fprintf(f, "MenuState::render failed to load fallback overlay: %s\n", SDL_GetError()); + fclose(f); + } + } + } + overlayTex = fallbackOverlay; + overlayW = fallbackW; + overlayH = fallbackH; + } - 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}); + if (overlayTex) { + { + FILE* f = fopen("tetris_trace.log", "a"); + if (f) { + fprintf(f, "MenuState::render overlay tex=%llu dims=%dx%d\n", + (unsigned long long)(uintptr_t)overlayTex, + overlayW, + overlayH); + fclose(f); + } + } + float texW = overlayW > 0 ? static_cast(overlayW) : 0.0f; + float texH = overlayH > 0 ? static_cast(overlayH) : 0.0f; + if (texW <= 0.0f || texH <= 0.0f) { + if (!SDL_GetTextureSize(overlayTex, &texW, &texH)) { + FILE* f = fopen("tetris_trace.log", "a"); + if (f) { + fprintf(f, "MenuState::render failed to query overlay size: %s\n", SDL_GetError()); + fclose(f); + } + texW = 0.0f; + texH = 0.0f; + } + } + if (texW > 0.0f && texH > 0.0f) { + const float drawH = LOGICAL_H; + const float scale = drawH / texH; + const float drawW = texW * scale; + SDL_FRect dst{ + (LOGICAL_W - drawW) * 0.5f + contentOffsetX, + contentOffsetY, + drawW, + drawH + }; + int renderResult = SDL_RenderTexture(renderer, overlayTex, nullptr, &dst); + if (renderResult < 0) { + FILE* f = fopen("tetris_trace.log", "a"); + if (f) { + fprintf(f, "MenuState::render failed to draw overlay: %s\n", SDL_GetError()); + fclose(f); + } + } + } + } else { + FILE* f = fopen("tetris_trace.log", "a"); + if (f) { + fprintf(f, "MenuState::render no overlay texture available\n"); + fclose(f); + } } // Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test) diff --git a/src/states/State.h b/src/states/State.h index 68bee3c..dc0a0b5 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -39,6 +39,9 @@ struct StateContext { // backgroundTex is set once in `main.cpp` and passed to states via this context. // Prefer reading this field instead of relying on any `extern SDL_Texture*` globals. SDL_Texture* blocksTex = nullptr; + SDL_Texture* mainScreenTex = nullptr; + int mainScreenW = 0; + int mainScreenH = 0; // Audio / SFX - forward declared types in main // Pointers to booleans/flags used by multiple states