new background for main screen

This commit is contained in:
2025-12-01 20:52:26 +01:00
parent cfbb4e4c86
commit 383b2e48ec
11 changed files with 417 additions and 85 deletions

View File

@ -44,6 +44,7 @@ set(TETRIS_SOURCES
src/persistence/Scores.cpp src/persistence/Scores.cpp
src/graphics/effects/Starfield.cpp src/graphics/effects/Starfield.cpp
src/graphics/effects/Starfield3D.cpp src/graphics/effects/Starfield3D.cpp
src/graphics/effects/SpaceWarp.cpp
src/graphics/ui/Font.cpp src/graphics/ui/Font.cpp
src/graphics/ui/HelpOverlay.cpp src/graphics/ui/HelpOverlay.cpp
src/graphics/renderers/GameRenderer.cpp src/graphics/renderers/GameRenderer.cpp

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -0,0 +1,149 @@
#include "SpaceWarp.h"
#include <algorithm>
#include <array>
#include <cmath>
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<float> dist(min, max);
return dist(rng);
}
static int randomIntInclusive(std::mt19937& rng, int min, int max) {
std::uniform_int_distribution<int> dist(min, max);
return dist(rng);
}
void SpaceWarp::respawn(WarpStar& star, bool randomDepth) {
float aspect = static_cast<float>(width) / static_cast<float>(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<Uint8>(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<Uint8>(colorValue);
if (settings.drawTrails) {
float trailAlphaFloat = alpha * settings.trailAlphaScale;
Uint8 trailAlpha = static_cast<Uint8>(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);
}

View File

@ -0,0 +1,69 @@
#pragma once
#include <SDL3/SDL.h>
#include <random>
#include <vector>
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<WarpStar> 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;
};

View File

@ -25,6 +25,7 @@
#include "persistence/Scores.h" #include "persistence/Scores.h"
#include "graphics/effects/Starfield.h" #include "graphics/effects/Starfield.h"
#include "graphics/effects/Starfield3D.h" #include "graphics/effects/Starfield3D.h"
#include "graphics/effects/SpaceWarp.h"
#include "graphics/ui/Font.h" #include "graphics/ui/Font.h"
#include "graphics/ui/HelpOverlay.h" #include "graphics/ui/HelpOverlay.h"
#include "gameplay/effects/LineEffect.h" #include "gameplay/effects/LineEffect.h"
@ -680,16 +681,15 @@ static void drawFireworks_impl(SDL_Renderer* renderer, SDL_Texture*) {
SDL_SetRenderDrawBlendMode(renderer, previousBlend); SDL_SetRenderDrawBlendMode(renderer, previousBlend);
} }
// External wrappers for use by other translation units (MenuState) // External wrappers retained for compatibility; now no-ops to disable the legacy fireworks effect.
// Expect callers to pass the blocks texture via StateContext so we avoid globals. void menu_drawFireworks(SDL_Renderer*, SDL_Texture*) {}
void menu_drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex) { drawFireworks_impl(renderer, blocksTex); } void menu_updateFireworks(double) {}
void menu_updateFireworks(double frameMs) { updateFireworks(frameMs); }
double menu_getLogoAnimCounter() { return logoAnimCounter; } double menu_getLogoAnimCounter() { return logoAnimCounter; }
int menu_getHoveredButton() { return hoveredButton; } int menu_getHoveredButton() { return hoveredButton; }
int main(int, char **) int main(int, char **)
{ {
// Initialize random seed for fireworks // Initialize random seed for procedural effects
srand(static_cast<unsigned int>(SDL_GetTicks())); srand(static_cast<unsigned int>(SDL_GetTicks()));
// Load settings // Load settings
@ -779,6 +779,8 @@ int main(int, char **)
starfield.init(200, LOGICAL_W, LOGICAL_H); starfield.init(200, LOGICAL_W, LOGICAL_H);
Starfield3D starfield3D; Starfield3D starfield3D;
starfield3D.init(LOGICAL_W, LOGICAL_H, 200); starfield3D.init(LOGICAL_W, LOGICAL_H, 200);
SpaceWarp spaceWarp;
spaceWarp.init(LOGICAL_W, LOGICAL_H, 420);
// Initialize line clearing effects // Initialize line clearing effects
LineEffect lineEffect; LineEffect lineEffect;
@ -794,6 +796,27 @@ int main(int, char **)
// Load menu background using SDL_image (prefers JPEG) // Load menu background using SDL_image (prefers JPEG)
SDL_Texture* backgroundTex = loadTextureFromImage(renderer, "assets/images/main_background.bmp"); 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. // Note: `backgroundTex` is owned by main and passed into `StateContext::backgroundTex` below.
// States should render using `ctx.backgroundTex` rather than accessing globals. // States should render using `ctx.backgroundTex` rather than accessing globals.
@ -971,6 +994,9 @@ int main(int, char **)
ctx.logoSmallH = logoSmallH; ctx.logoSmallH = logoSmallH;
ctx.backgroundTex = backgroundTex; ctx.backgroundTex = backgroundTex;
ctx.blocksTex = blocksTex; ctx.blocksTex = blocksTex;
ctx.mainScreenTex = mainScreenTex;
ctx.mainScreenW = mainScreenW;
ctx.mainScreenH = mainScreenH;
ctx.musicEnabled = &musicEnabled; ctx.musicEnabled = &musicEnabled;
ctx.startLevelSelection = &startLevelSelection; ctx.startLevelSelection = &startLevelSelection;
ctx.hoveredButton = &hoveredButton; ctx.hoveredButton = &hoveredButton;
@ -1565,21 +1591,25 @@ int main(int, char **)
} }
previousState = state; previousState = state;
// Update starfields based on current state // Update background effects
if (state == AppState::Loading) { if (state == AppState::Loading) {
starfield3D.update(float(frameMs / 1000.0f)); starfield3D.update(float(frameMs / 1000.0f));
starfield3D.resize(logicalVP.w, logicalVP.h); // Update for window resize starfield3D.resize(winW, winH);
} else { } else {
starfield.update(float(frameMs / 1000.0f), logicalVP.x * 2 + logicalVP.w, logicalVP.y * 2 + logicalVP.h); 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 // Advance level background fade if a next texture is queued
updateLevelBackgroundFade(levelBackgrounds, float(frameMs)); updateLevelBackgroundFade(levelBackgrounds, float(frameMs));
// Update intro animations // Update intro animations
if (state == AppState::Menu) { if (state == AppState::Menu) {
logoAnimCounter += frameMs * 0.0008; // Animation speed logoAnimCounter += frameMs * 0.0008; // Animation speed
updateFireworks(frameMs);
} }
// --- Per-state update hooks (allow states to manage logic incrementally) // --- Per-state update hooks (allow states to manage logic incrementally)
@ -1671,7 +1701,7 @@ int main(int, char **)
// --- Render --- // --- Render ---
SDL_SetRenderViewport(renderer, nullptr); SDL_SetRenderViewport(renderer, nullptr);
SDL_SetRenderDrawColor(renderer, 12, 12, 16, 255); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer); SDL_RenderClear(renderer);
// Draw level-based background for gameplay, starfield for other states // Draw level-based background for gameplay, starfield for other states
@ -1682,8 +1712,32 @@ int main(int, char **)
} else if (state == AppState::Loading) { } else if (state == AppState::Loading) {
// Use 3D starfield for loading screen (full screen) // Use 3D starfield for loading screen (full screen)
starfield3D.draw(renderer); starfield3D.draw(renderer);
} else if (state == AppState::Menu || state == AppState::LevelSelector || state == AppState::Options) { } else if (state == AppState::Menu) {
// Use static background for menu, stretched to window; no starfield on sides // Space flyover backdrop for the main screen
spaceWarp.draw(renderer, 1.0f);
if (mainScreenTex) {
float texW = mainScreenW > 0 ? static_cast<float>(mainScreenW) : 0.0f;
float texH = mainScreenH > 0 ? static_cast<float>(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<float>(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) { if (backgroundTex) {
SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH }; SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH };
SDL_RenderTexture(renderer, backgroundTex, nullptr, &fullRect); SDL_RenderTexture(renderer, backgroundTex, nullptr, &fullRect);
@ -2021,6 +2075,8 @@ int main(int, char **)
SDL_DestroyTexture(logoTex); SDL_DestroyTexture(logoTex);
if (backgroundTex) if (backgroundTex)
SDL_DestroyTexture(backgroundTex); SDL_DestroyTexture(backgroundTex);
if (mainScreenTex)
SDL_DestroyTexture(mainScreenTex);
resetLevelBackgrounds(levelBackgrounds); resetLevelBackgrounds(levelBackgrounds);
if (blocksTex) if (blocksTex)
SDL_DestroyTexture(blocksTex); SDL_DestroyTexture(blocksTex);

View File

@ -19,6 +19,8 @@
// `ctx.musicEnabled` and `ctx.hoveredButton` instead to avoid globals. // `ctx.musicEnabled` and `ctx.hoveredButton` instead to avoid globals.
// Menu helper wrappers are declared in a shared header implemented in main.cpp // Menu helper wrappers are declared in a shared header implemented in main.cpp
#include "../audio/MenuWrappers.h" #include "../audio/MenuWrappers.h"
#include "../utils/ImagePathResolver.h"
#include <SDL3_image/SDL_image.h>
MenuState::MenuState(StateContext& ctx) : State(ctx) {} MenuState::MenuState(StateContext& ctx) : State(ctx) {}
@ -159,9 +161,6 @@ void MenuState::handleEvent(const SDL_Event& e) {
void MenuState::update(double frameMs) { void MenuState::update(double frameMs) {
// Update logo animation counter // Update logo animation counter
GlobalState::instance().logoAnimCounter += frameMs; GlobalState::instance().logoAnimCounter += frameMs;
// Update fireworks particles
GlobalState::instance().updateFireworks(frameMs);
} }
void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { 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; float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
// Background is drawn by main (stretched to the full window) to avoid double-draw. // 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); } FILE* f = fopen("tetris_trace.log", "a");
} if (f) {
if (logoToUse) { fprintf(f, "MenuState::render ctx.mainScreenTex=%llu (w=%d h=%d)\n",
// Use dimensions provided by the shared context when available (unsigned long long)(uintptr_t)ctx.mainScreenTex,
int texW = (logoToUse == ctx.logoSmallTex && ctx.logoSmallW > 0) ? ctx.logoSmallW : 872; ctx.mainScreenW,
int texH = (logoToUse == ctx.logoSmallTex && ctx.logoSmallH > 0) ? ctx.logoSmallH : 273; ctx.mainScreenH);
float maxW = LOGICAL_W * 0.6f; fclose(f);
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); }
} }
} }
// 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; FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
float topPlayersY = LOGICAL_H * 0.24f + contentOffsetY;
if (useFont) { 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"; 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; float titleX = (LOGICAL_W - (float)tW) * 0.5f + contentOffsetX;
useFont->draw(renderer, titleX, topPlayersY, title, 1.8f, SDL_Color{255, 220, 0, 255}); 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.0f;
float scoresStartY = topPlayersY + 70; // more spacing under title
static const std::vector<ScoreEntry> EMPTY_SCORES; static const std::vector<ScoreEntry> EMPTY_SCORES;
const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES; const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES;
size_t maxDisplay = std::min(hs.size(), size_t(12)); size_t maxDisplay = std::min(hs.size(), size_t(12));
// Draw table header
if (useFont) { if (useFont) {
float cx = LOGICAL_W * 0.5f + contentOffsetX; float cx = LOGICAL_W * 0.5f + contentOffsetX;
float colX[] = { cx - 280, cx - 180, cx - 20, cx + 90, cx + 200, cx + 300 }; 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[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[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}); 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]; // Draw the sci-fi overlay that sits above the scoreboard but below the buttons
std::snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1); SDL_Texture* overlayTex = ctx.mainScreenTex;
if (useFont) useFont->draw(renderer, colX[0], y, rankStr, 1.0f, SDL_Color{220, 220, 230, 255}); 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 (!overlayTex) {
if (useFont) useFont->draw(renderer, colX[2], y, scoreStr, 1.0f, SDL_Color{220, 220, 230, 255}); 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<int>(tmpW);
fallbackH = static_cast<int>(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 (overlayTex) {
if (useFont) useFont->draw(renderer, colX[3], y, linesStr, 1.0f, SDL_Color{220, 220, 230, 255}); {
FILE* f = fopen("tetris_trace.log", "a");
char levelStr[8]; std::snprintf(levelStr, sizeof(levelStr), "%d", hs[i].level); if (f) {
if (useFont) useFont->draw(renderer, colX[4], y, levelStr, 1.0f, SDL_Color{220, 220, 230, 255}); fprintf(f, "MenuState::render overlay tex=%llu dims=%dx%d\n",
(unsigned long long)(uintptr_t)overlayTex,
char timeStr[16]; int mins = int(hs[i].timeSec) / 60; int secs = int(hs[i].timeSec) % 60; overlayW,
std::snprintf(timeStr, sizeof(timeStr), "%d:%02d", mins, secs); overlayH);
if (useFont) useFont->draw(renderer, colX[5], y, timeStr, 1.0f, SDL_Color{220, 220, 230, 255}); fclose(f);
}
}
float texW = overlayW > 0 ? static_cast<float>(overlayW) : 0.0f;
float texH = overlayH > 0 ? static_cast<float>(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) // Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test)

View File

@ -39,6 +39,9 @@ struct StateContext {
// backgroundTex is set once in `main.cpp` and passed to states via this context. // 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. // Prefer reading this field instead of relying on any `extern SDL_Texture*` globals.
SDL_Texture* blocksTex = nullptr; SDL_Texture* blocksTex = nullptr;
SDL_Texture* mainScreenTex = nullptr;
int mainScreenW = 0;
int mainScreenH = 0;
// Audio / SFX - forward declared types in main // Audio / SFX - forward declared types in main
// Pointers to booleans/flags used by multiple states // Pointers to booleans/flags used by multiple states