- **Visual Effects**: Upgraded line clear particles to use the game's block texture instead of simple circles, matching the reference web game's aesthetic. - **Particle Physics**: Tuned particle velocity, gravity, and fade rates for a more dynamic explosion effect. - **Rendering Integration**: Updated [main.cpp](cci:7://file:///d:/Sites/Work/tetris/src/main.cpp:0:0-0:0) and `GameRenderer` to pass the block texture to the effect system and correctly trigger animations upon line completion. - **Menu UI**: Fixed [MenuState](cci:1://file:///d:/Sites/Work/tetris/src/states/MenuState.cpp:19:0-19:55) layout calculations to use fixed logical dimensions (1200x1000), ensuring consistent centering and alignment of the logo, buttons, and settings icon across different window sizes. - **Code Cleanup**: Refactored `PlayingState` to delegate effect triggering to the rendering layer where correct screen coordinates are available.
222 lines
12 KiB
C++
222 lines
12 KiB
C++
#include "MenuState.h"
|
|
#include "persistence/Scores.h"
|
|
#include "graphics/Font.h"
|
|
#include "../core/GlobalState.h"
|
|
#include "../audio/Audio.h"
|
|
#include <SDL3/SDL.h>
|
|
#include <cstdio>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
|
|
// This allows the UI to adapt when the window is resized or goes fullscreen
|
|
|
|
// Shared flags and resources are provided via StateContext `ctx`.
|
|
// Removed fragile extern declarations and use `ctx.showLevelPopup`, `ctx.showSettingsPopup`,
|
|
// `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"
|
|
|
|
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
|
|
|
|
void MenuState::onEnter() {
|
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called");
|
|
}
|
|
|
|
void MenuState::onExit() {
|
|
}
|
|
|
|
void MenuState::handleEvent(const SDL_Event& e) {
|
|
// 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) {
|
|
// Use fixed logical dimensions to match main.cpp and ensure consistent layout
|
|
// This prevents the UI from floating apart on wide/tall screens
|
|
const float LOGICAL_W = 1200.f;
|
|
const float LOGICAL_H = 1000.f;
|
|
|
|
// Trace entry to persistent log for debugging abrupt exit/crash during render
|
|
{
|
|
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render entry\n"); fclose(f); }
|
|
}
|
|
|
|
// Compute content offsets (same approach as main.cpp for proper centering)
|
|
float winW = (float)logicalVP.w;
|
|
float winH = (float)logicalVP.h;
|
|
float contentW = LOGICAL_W * logicalScale;
|
|
float contentH = LOGICAL_H * logicalScale;
|
|
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
|
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); }
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
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);
|
|
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
|
|
const auto &hs = ctx.scores ? ctx.scores->all() : *(new std::vector<ScoreEntry>());
|
|
size_t maxDisplay = std::min(hs.size(), size_t(12));
|
|
|
|
// Draw table header
|
|
if (useFont) {
|
|
float cx = LOGICAL_W * 0.5f + contentOffsetX;
|
|
float colX[] = { cx - 280, cx - 180, cx - 20, cx + 90, cx + 200, cx + 300 };
|
|
useFont->draw(renderer, colX[0], scoresStartY - 28, "RANK", 1.1f, SDL_Color{200,200,220,255});
|
|
useFont->draw(renderer, colX[1], scoresStartY - 28, "PLAYER", 1.1f, SDL_Color{200,200,220,255});
|
|
useFont->draw(renderer, colX[2], scoresStartY - 28, "SCORE", 1.1f, SDL_Color{200,200,220,255});
|
|
useFont->draw(renderer, colX[3], scoresStartY - 28, "LINES", 1.1f, SDL_Color{200,200,220,255});
|
|
useFont->draw(renderer, colX[4], scoresStartY - 28, "LEVEL", 1.1f, SDL_Color{200,200,220,255});
|
|
useFont->draw(renderer, colX[5], scoresStartY - 28, "TIME", 1.1f, SDL_Color{200,200,220,255});
|
|
}
|
|
|
|
// 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});
|
|
|
|
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)
|
|
// Use the contentW calculated at the top with content offsets
|
|
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) {
|
|
{
|
|
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render drawing buttons; pixelFont=%llu\n", (unsigned long long)(uintptr_t)ctx.pixelFont); fclose(f); }
|
|
}
|
|
char levelBtnText[32];
|
|
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
|
|
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
|
// Draw simple styled buttons (replicating menu_drawMenuButton)
|
|
auto drawMenuButtonLocal = [&](SDL_Renderer* r, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, SDL_Color bg, SDL_Color border){
|
|
float x = cx - w/2; float y = cy - h/2;
|
|
SDL_SetRenderDrawColor(r, border.r, border.g, border.b, border.a);
|
|
SDL_FRect br{ x-6, y-6, w+12, h+12 }; SDL_RenderFillRect(r, &br);
|
|
SDL_SetRenderDrawColor(r, 255,255,255,255); SDL_FRect br2{ x-4, y-4, w+8, h+8 }; SDL_RenderFillRect(r, &br2);
|
|
SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a); SDL_FRect br3{ x, y, w, h }; SDL_RenderFillRect(r, &br3);
|
|
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(r, tx+2, ty+2, label, textScale, SDL_Color{0,0,0,180});
|
|
font.draw(r, tx, ty, label, textScale, SDL_Color{255,255,255,255});
|
|
};
|
|
drawMenuButtonLocal(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});
|
|
{
|
|
}
|
|
drawMenuButtonLocal(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});
|
|
{
|
|
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after draw LEVEL button\n"); fclose(f); }
|
|
}
|
|
}
|
|
|
|
// Popups (settings only - level popup is now a separate state)
|
|
if (ctx.showSettingsPopup && *ctx.showSettingsPopup) {
|
|
// draw settings popup inline
|
|
bool musicOn = ctx.musicEnabled ? *ctx.musicEnabled : true;
|
|
float popupW = 350, popupH = 260;
|
|
float popupX = (LOGICAL_W - popupW) / 2;
|
|
float popupY = (LOGICAL_H - popupH) / 2;
|
|
SDL_SetRenderDrawColor(renderer, 0,0,0,128); SDL_FRect overlay{0,0,(float)LOGICAL_W,(float)LOGICAL_H}; SDL_RenderFillRect(renderer, &overlay);
|
|
SDL_SetRenderDrawColor(renderer, 100,120,160,255); SDL_FRect bord{popupX-4,popupY-4,popupW+8,popupH+8}; SDL_RenderFillRect(renderer, &bord);
|
|
SDL_SetRenderDrawColor(renderer, 40,50,70,255); SDL_FRect body{popupX,popupY,popupW,popupH}; SDL_RenderFillRect(renderer, &body);
|
|
ctx.font->draw(renderer, popupX + 20, popupY + 20, "SETTINGS", 2.0f, SDL_Color{255,220,0,255});
|
|
ctx.font->draw(renderer, popupX + 20, popupY + 70, "MUSIC:", 1.5f, SDL_Color{255,255,255,255});
|
|
ctx.font->draw(renderer, popupX + 120, popupY + 70, musicOn ? "ON" : "OFF", 1.5f, musicOn ? SDL_Color{0,255,0,255} : SDL_Color{255,0,0,255});
|
|
ctx.font->draw(renderer, popupX + 20, popupY + 100, "SOUND FX:", 1.5f, SDL_Color{255,255,255,255});
|
|
ctx.font->draw(renderer, popupX + 140, popupY + 100, "ON", 1.5f, SDL_Color{0,255,0,255});
|
|
ctx.font->draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, SDL_Color{200,200,220,255});
|
|
ctx.font->draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, SDL_Color{200,200,220,255});
|
|
ctx.font->draw(renderer, popupX + 20, popupY + 190, "N = PLAY LETS_GO", 1.0f, SDL_Color{200,200,220,255});
|
|
ctx.font->draw(renderer, popupX + 20, popupY + 210, "ESC = CLOSE", 1.0f, SDL_Color{200,200,220,255});
|
|
}
|
|
// Trace exit
|
|
{
|
|
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render exit\n"); fclose(f); }
|
|
}
|
|
}
|