diff --git a/assets/images/main_screen.png b/assets/images/main_screen.png index 3f593b8..fb37204 100644 Binary files a/assets/images/main_screen.png and b/assets/images/main_screen.png differ diff --git a/assets/images/main_screen_old.png b/assets/images/main_screen_old.png new file mode 100644 index 0000000..3f593b8 Binary files /dev/null and b/assets/images/main_screen_old.png differ diff --git a/src/graphics/effects/SpaceWarp.cpp b/src/graphics/effects/SpaceWarp.cpp index 31f2cb0..4ff5a0c 100644 --- a/src/graphics/effects/SpaceWarp.cpp +++ b/src/graphics/effects/SpaceWarp.cpp @@ -107,8 +107,22 @@ void SpaceWarp::spawnComet() { float normalizedAspect = std::max(aspect, MIN_ASPECT); float xRange = settings.baseSpawnRange * 1.2f * (aspect >= 1.0f ? aspect : 1.0f); float yRange = settings.baseSpawnRange * 1.2f * (aspect >= 1.0f ? 1.0f : (1.0f / normalizedAspect)); - comet.x = randomRange(-xRange, xRange); - comet.y = randomRange(-yRange, yRange); + // Avoid spawning comets exactly on (or extremely near) the view axis, + // which can project to a nearly static bright dot. + const float axisMinFrac = 0.06f; + bool axisOk = false; + for (int attempt = 0; attempt < 10 && !axisOk; ++attempt) { + comet.x = randomRange(-xRange, xRange); + comet.y = randomRange(-yRange, yRange); + float nx = comet.x / std::max(xRange, 0.0001f); + float ny = comet.y / std::max(yRange, 0.0001f); + axisOk = (nx * nx + ny * ny) >= (axisMinFrac * axisMinFrac); + } + if (!axisOk) { + float ang = randomRange(0.0f, 6.28318530718f); + comet.x = std::cos(ang) * xRange * axisMinFrac; + comet.y = std::sin(ang) * yRange * axisMinFrac; + } comet.z = randomRange(minDepth + 4.0f, maxDepth); float baseSpeed = randomRange(settings.minSpeed, settings.maxSpeed); float multiplier = randomRange(settings.cometSpeedMultiplierMin, settings.cometSpeedMultiplierMax); @@ -154,9 +168,24 @@ void SpaceWarp::respawn(WarpStar& star, bool randomDepth) { 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; + // Avoid axis-aligned stars (x≈0,y≈0) which can project to a static, bright center dot. + const float axisMinFrac = 0.06f; + bool axisOk = false; + for (int attempt = 0; attempt < 10 && !axisOk; ++attempt) { + star.x = randomRange(-xRange, xRange); + star.y = randomRange(-yRange, yRange); + float nx = star.x / std::max(xRange, 0.0001f); + float ny = star.y / std::max(yRange, 0.0001f); + axisOk = (nx * nx + ny * ny) >= (axisMinFrac * axisMinFrac); + } + if (!axisOk) { + float ang = randomRange(0.0f, 6.28318530718f); + star.x = std::cos(ang) * xRange * axisMinFrac; + star.y = std::sin(ang) * yRange * axisMinFrac; + } + + // Keep z slightly above minDepth so projection never starts from the exact singular plane. + star.z = randomDepth ? randomRange(minDepth + 0.25f, 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}; @@ -253,6 +282,13 @@ void SpaceWarp::update(float deltaSeconds) { continue; } + // If a star projects to (near) the visual center, it can appear perfectly static + // during straight-line flight. Replace it to avoid the "big static star" artifact. + if (std::abs(sx - centerX) < 1.25f && std::abs(sy - centerY) < 1.25f) { + respawn(star, true); + continue; + } + star.prevScreenX = star.screenX; star.prevScreenY = star.screenY; star.screenX = sx; diff --git a/src/graphics/effects/Starfield3D.cpp b/src/graphics/effects/Starfield3D.cpp index 06030c3..e9b65a1 100644 --- a/src/graphics/effects/Starfield3D.cpp +++ b/src/graphics/effects/Starfield3D.cpp @@ -68,9 +68,24 @@ void Starfield3D::setRandomDirection(Star3D& star) { void Starfield3D::updateStar(int index) { Star3D& star = stars[index]; - - star.x = randomFloat(-25.0f, 25.0f); - star.y = randomFloat(-25.0f, 25.0f); + + // Avoid spawning stars on (or very near) the view axis. A star with x≈0 and y≈0 + // projects to the exact center, and when it happens to be bright it looks like a + // static "big" star. + constexpr float SPAWN_RANGE = 25.0f; + constexpr float MIN_AXIS_RADIUS = 2.5f; // in star-space units + for (int attempt = 0; attempt < 8; ++attempt) { + star.x = randomFloat(-SPAWN_RANGE, SPAWN_RANGE); + star.y = randomFloat(-SPAWN_RANGE, SPAWN_RANGE); + if ((star.x * star.x + star.y * star.y) >= (MIN_AXIS_RADIUS * MIN_AXIS_RADIUS)) { + break; + } + } + // If we somehow still ended up too close, push it out deterministically. + if ((star.x * star.x + star.y * star.y) < (MIN_AXIS_RADIUS * MIN_AXIS_RADIUS)) { + star.x = (star.x < 0.0f ? -1.0f : 1.0f) * MIN_AXIS_RADIUS; + star.y = (star.y < 0.0f ? -1.0f : 1.0f) * MIN_AXIS_RADIUS; + } star.z = randomFloat(1.0f, MAX_DEPTH); // Give stars initial velocities in all possible directions @@ -91,6 +106,15 @@ void Starfield3D::updateStar(int index) { star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f); } } + + // Ensure newly spawned stars have some lateral drift so they don't appear to + // "stick" near the center line. + if (std::abs(star.vx) < 0.02f && std::abs(star.vy) < 0.02f) { + const float sx = (star.x < 0.0f ? -1.0f : 1.0f); + const float sy = (star.y < 0.0f ? -1.0f : 1.0f); + star.vx = sx * randomFloat(0.04f, 0.14f); + star.vy = sy * randomFloat(0.04f, 0.14f); + } star.targetVx = star.vx; star.targetVy = star.vy; diff --git a/src/main.cpp b/src/main.cpp index 8a21591..3f0c0bc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1001,6 +1001,10 @@ int main(int, char **) // HELP - show inline help HUD in the MenuState if (menuState) menuState->showHelpPanel(true); break; + case ui::BottomMenuItem::About: + // ABOUT - show inline about HUD in the MenuState + if (menuState) menuState->showAboutPanel(true); + break; case ui::BottomMenuItem::Exit: showExitConfirmPopup = true; exitPopupSelectedButton = 1; @@ -1792,6 +1796,9 @@ int main(int, char **) drawH }; SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND); + // Use linear filtering for the scaled overlay to avoid single-pixel aliasing + // artifacts (e.g. a tiny static dot) when the PNG is resized. + SDL_SetTextureScaleMode(mainScreenTex, SDL_SCALEMODE_LINEAR); SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst); } SDL_SetRenderViewport(renderer, &logicalVP); diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 6a8c457..9b20f9d 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -112,6 +112,11 @@ MenuState::MenuState(StateContext& ctx) : State(ctx) {} void MenuState::showHelpPanel(bool show) { if (show) { if (!helpPanelVisible && !helpPanelAnimating) { + // Avoid overlapping panels + if (aboutPanelVisible && !aboutPanelAnimating) { + aboutPanelAnimating = true; + aboutDirection = -1; + } helpPanelAnimating = true; helpDirection = 1; helpScroll = 0.0; @@ -124,6 +129,38 @@ void MenuState::showHelpPanel(bool show) { } } +void MenuState::showAboutPanel(bool show) { + if (show) { + if (!aboutPanelVisible && !aboutPanelAnimating) { + // Avoid overlapping panels + if (helpPanelVisible && !helpPanelAnimating) { + helpPanelAnimating = true; + helpDirection = -1; + } + if (optionsVisible && !optionsAnimating) { + optionsAnimating = true; + optionsDirection = -1; + } + if (levelPanelVisible && !levelPanelAnimating) { + levelPanelAnimating = true; + levelDirection = -1; + } + if (exitPanelVisible && !exitPanelAnimating) { + exitPanelAnimating = true; + exitDirection = -1; + if (ctx.showExitConfirmPopup) *ctx.showExitConfirmPopup = false; + } + aboutPanelAnimating = true; + aboutDirection = 1; + } + } else { + if (aboutPanelVisible && !aboutPanelAnimating) { + aboutPanelAnimating = true; + aboutDirection = -1; + } + } +} + void MenuState::onEnter() { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called"); if (ctx.showExitConfirmPopup) { @@ -349,6 +386,26 @@ void MenuState::handleEvent(const SDL_Event& e) { } } + // If the inline about HUD is visible and not animating, capture navigation + if (aboutPanelVisible && !aboutPanelAnimating) { + switch (e.key.scancode) { + case SDL_SCANCODE_ESCAPE: + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_KP_ENTER: + case SDL_SCANCODE_SPACE: + aboutPanelAnimating = true; aboutDirection = -1; + return; + case SDL_SCANCODE_LEFT: + case SDL_SCANCODE_RIGHT: + case SDL_SCANCODE_UP: + case SDL_SCANCODE_DOWN: + aboutPanelAnimating = true; aboutDirection = -1; + break; + default: + return; + } + } + // If inline level HUD visible and not animating, capture navigation if (levelPanelVisible && !levelPanelAnimating) { // Start navigation from tentative hover if present, otherwise from committed selection @@ -385,7 +442,7 @@ void MenuState::handleEvent(const SDL_Event& e) { case SDL_SCANCODE_LEFT: case SDL_SCANCODE_UP: { - const int total = 5; + const int total = 6; selectedButton = (selectedButton + total - 1) % total; // brief bright flash on navigation buttonFlash = 1.0; @@ -394,7 +451,7 @@ void MenuState::handleEvent(const SDL_Event& e) { case SDL_SCANCODE_RIGHT: case SDL_SCANCODE_DOWN: { - const int total = 5; + const int total = 6; selectedButton = (selectedButton + 1) % total; // brief bright flash on navigation buttonFlash = 1.0; @@ -444,6 +501,16 @@ void MenuState::handleEvent(const SDL_Event& e) { } break; case 4: + // Toggle the inline ABOUT HUD (show/hide) + if (!aboutPanelVisible && !aboutPanelAnimating) { + aboutPanelAnimating = true; + aboutDirection = 1; + } else if (aboutPanelVisible && !aboutPanelAnimating) { + aboutPanelAnimating = true; + aboutDirection = -1; + } + break; + case 5: // Show the inline exit HUD if (!exitPanelVisible && !exitPanelAnimating) { exitPanelAnimating = true; @@ -540,6 +607,21 @@ void MenuState::update(double frameMs) { } } + // Advance about panel animation if active + if (aboutPanelAnimating) { + double delta = (frameMs / aboutTransitionDurationMs) * static_cast(aboutDirection); + aboutTransition += delta; + if (aboutTransition >= 1.0) { + aboutTransition = 1.0; + aboutPanelVisible = true; + aboutPanelAnimating = false; + } else if (aboutTransition <= 0.0) { + aboutTransition = 0.0; + aboutPanelVisible = false; + aboutPanelAnimating = false; + } + } + // Animate level selection highlight position toward the selected cell center if (levelTransition > 0.0 && (lastLogicalScale > 0.0f)) { // Recompute same grid geometry used in render to find target center @@ -665,14 +747,18 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi const float moveAmount = 420.0f; // increased so lower score rows slide further up // Compute eased transition and delta to shift highscores when either options, level, or exit HUD is shown. - float combinedTransition = static_cast(std::max(std::max(std::max(optionsTransition, levelTransition), exitTransition), helpTransition)); + float combinedTransition = static_cast(std::max( + std::max(std::max(optionsTransition, levelTransition), exitTransition), + std::max(helpTransition, aboutTransition) + )); float eased = combinedTransition * combinedTransition * (3.0f - 2.0f * combinedTransition); // cubic smoothstep float panelDelta = eased * moveAmount; // Draw a larger centered logo above the highscores area, then a small "TOP PLAYER" label - // Move logo a bit lower for better spacing + // Move the whole block slightly up to better match the main screen overlay framing. float menuYOffset = LOGICAL_H * 0.03f; // same offset used for buttons - float topPlayersY = LOGICAL_H * 0.20f + contentOffsetY - panelDelta + menuYOffset; + float scoresYOffset = -LOGICAL_H * 0.05f; + float topPlayersY = LOGICAL_H * 0.20f + contentOffsetY - panelDelta + menuYOffset + scoresYOffset; float scoresStartY = topPlayersY; if (useFont) { // Preferred logo texture (full) if present, otherwise the small logo @@ -1185,8 +1271,47 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi int w=0,h=0; f->measure(entry.description, 0.62f, w, h); cursorY += static_cast(h) + 16.0f; } + + // (rest of help render continues below) // Add a larger gap between sections cursorY += 22.0f; + + // Draw inline ABOUT HUD (no boxed background) — simple main info + if (aboutTransition > 0.0) { + float easedA = static_cast(aboutTransition); + easedA = easedA * easedA * (3.0f - 2.0f * easedA); + const float PW = std::min(520.0f, LOGICAL_W * 0.65f); + const float PH = std::min(320.0f, LOGICAL_H * 0.60f); + float panelBaseX = (LOGICAL_W - PW) * 0.5f + contentOffsetX; + float panelBaseY = (LOGICAL_H - PH) * 0.5f + contentOffsetY - (LOGICAL_H * 0.10f); + float slideAmount = LOGICAL_H * 0.42f; + float panelY = panelBaseY + (1.0f - easedA) * slideAmount; + + FontAtlas* f = ctx.pixelFont ? ctx.pixelFont : ctx.font; + if (f) { + f->draw(renderer, panelBaseX + 12.0f, panelY + 6.0f, "ABOUT", 1.25f, SDL_Color{255,220,0,255}); + + float x = panelBaseX + 16.0f; + float y = panelY + 52.0f; + const float lineGap = 30.0f; + const SDL_Color textCol{200, 210, 230, 255}; + const SDL_Color keyCol{255, 255, 255, 255}; + + f->draw(renderer, x, y, "SDL3 TETRIS", 1.05f, keyCol); y += lineGap; + f->draw(renderer, x, y, "C++20 / SDL3 / SDL3_ttf", 0.80f, textCol); y += lineGap + 6.0f; + + f->draw(renderer, x, y, "GAMEPLAY", 0.85f, SDL_Color{180,200,255,255}); y += lineGap; + f->draw(renderer, x, y, "H Hold / swap current piece", 0.78f, textCol); y += lineGap; + f->draw(renderer, x, y, "SPACE Hard drop", 0.78f, textCol); y += lineGap; + f->draw(renderer, x, y, "P Pause", 0.78f, textCol); y += lineGap + 6.0f; + + f->draw(renderer, x, y, "UI", 0.85f, SDL_Color{180,200,255,255}); y += lineGap; + f->draw(renderer, x, y, "F1 Toggle help overlay", 0.78f, textCol); y += lineGap; + f->draw(renderer, x, y, "ESC Back / exit prompt", 0.78f, textCol); y += lineGap + 10.0f; + + f->draw(renderer, x, y, "PRESS ESC OR ARROW KEYS TO RETURN", 0.75f, SDL_Color{215,220,240,255}); + } + } }; float leftCursor = panelY + 48.0f - static_cast(helpScroll); diff --git a/src/states/MenuState.h b/src/states/MenuState.h index 9b2a766..7522975 100644 --- a/src/states/MenuState.h +++ b/src/states/MenuState.h @@ -17,9 +17,11 @@ public: void renderMainButtonTop(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP); // Show or hide the inline HELP panel (menu-style) void showHelpPanel(bool show); + // Show or hide the inline ABOUT panel (menu-style) + void showAboutPanel(bool show); private: - int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = HELP, 4 = EXIT + int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = HELP, 4 = ABOUT, 5 = EXIT // Button icons (optional - will use text if nullptr) SDL_Texture* playIcon = nullptr; @@ -85,4 +87,11 @@ private: double helpTransitionDurationMs = 360.0; int helpDirection = 1; // 1 show, -1 hide double helpScroll = 0.0; // vertical scroll offset for content + + // About submenu (inline HUD like Help) + bool aboutPanelVisible = false; + bool aboutPanelAnimating = false; + double aboutTransition = 0.0; // 0..1 + double aboutTransitionDurationMs = 360.0; + int aboutDirection = 1; // 1 show, -1 hide }; diff --git a/src/ui/BottomMenu.cpp b/src/ui/BottomMenu.cpp index 27b4059..1910ca4 100644 --- a/src/ui/BottomMenu.cpp +++ b/src/ui/BottomMenu.cpp @@ -25,7 +25,8 @@ BottomMenu buildBottomMenu(const MenuLayoutParams& params, int startLevel) { menu.buttons[1] = Button{ BottomMenuItem::Level, rects[1], levelBtnText, true }; menu.buttons[2] = Button{ BottomMenuItem::Options, rects[2], "OPTIONS", true }; menu.buttons[3] = Button{ BottomMenuItem::Help, rects[3], "HELP", true }; - menu.buttons[4] = Button{ BottomMenuItem::Exit, rects[4], "EXIT", true }; + menu.buttons[4] = Button{ BottomMenuItem::About, rects[4], "ABOUT", true }; + menu.buttons[5] = Button{ BottomMenuItem::Exit, rects[5], "EXIT", true }; return menu; } @@ -73,15 +74,17 @@ void renderBottomMenu(SDL_Renderer* renderer, } } - // '+' separators between the bottom HUD buttons (indices 1..4) + // '+' separators between the bottom HUD buttons (indices 1..last) { SDL_BlendMode prevBlend = SDL_BLENDMODE_NONE; SDL_GetRenderDrawBlendMode(renderer, &prevBlend); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(renderer, 120, 220, 255, static_cast(std::round(180.0 * baseMul))); - float y = menu.buttons[1].rect.y + menu.buttons[1].rect.h * 0.5f; - for (int i = 1; i < 4; ++i) { + const int firstSmall = 1; + const int lastSmall = MENU_BTN_COUNT - 1; + float y = menu.buttons[firstSmall].rect.y + menu.buttons[firstSmall].rect.h * 0.5f; + for (int i = firstSmall; i < lastSmall; ++i) { float x = (menu.buttons[i].rect.x + menu.buttons[i].rect.w + menu.buttons[i + 1].rect.x) * 0.5f; SDL_RenderLine(renderer, x - 4.0f, y, x + 4.0f, y); SDL_RenderLine(renderer, x, y - 4.0f, x, y + 4.0f); diff --git a/src/ui/BottomMenu.h b/src/ui/BottomMenu.h index 7627232..3f54fbe 100644 --- a/src/ui/BottomMenu.h +++ b/src/ui/BottomMenu.h @@ -18,7 +18,8 @@ enum class BottomMenuItem : int { Level = 1, Options = 2, Help = 3, - Exit = 4, + About = 4, + Exit = 5, }; struct Button { @@ -35,8 +36,8 @@ struct BottomMenu { BottomMenu buildBottomMenu(const MenuLayoutParams& params, int startLevel); // Draws the cockpit HUD menu (PLAY + 4 bottom items) using existing UIRenderer primitives. -// hoveredIndex: -1..4 -// selectedIndex: 0..4 (keyboard selection) +// hoveredIndex: -1..5 +// selectedIndex: 0..5 (keyboard selection) // alphaMul: 0..1 (overall group alpha) void renderBottomMenu(SDL_Renderer* renderer, FontAtlas* font, diff --git a/src/ui/MenuLayout.cpp b/src/ui/MenuLayout.cpp index 84dae86..5cf6560 100644 --- a/src/ui/MenuLayout.cpp +++ b/src/ui/MenuLayout.cpp @@ -5,7 +5,7 @@ namespace ui { -std::array computeMenuButtonRects(const MenuLayoutParams& p) { +std::array computeMenuButtonRects(const MenuLayoutParams& p) { const float LOGICAL_W = static_cast(p.logicalW); const float LOGICAL_H = static_cast(p.logicalH); float contentOffsetX = (p.winW - LOGICAL_W * p.logicalScale) * 0.5f / p.logicalScale; @@ -13,7 +13,7 @@ std::array computeMenuButtonRects(const MenuLayoutParams& p) { // Cockpit HUD layout (matches main_screen art): // - A big centered PLAY button - // - A second row of 4 smaller buttons: LEVEL / OPTIONS / HELP / EXIT + // - A second row of 5 smaller buttons: LEVEL / OPTIONS / HELP / ABOUT / EXIT const float marginX = std::max(24.0f, LOGICAL_W * 0.03f); const float marginBottom = std::max(26.0f, LOGICAL_H * 0.03f); const float availableW = std::max(120.0f, LOGICAL_W - marginX * 2.0f); @@ -25,7 +25,8 @@ std::array computeMenuButtonRects(const MenuLayoutParams& p) { float smallSpacing = 28.0f; // Scale down for narrow windows so nothing goes offscreen. - float smallTotal = smallW * 4.0f + smallSpacing * 3.0f; + const int smallCount = MENU_BTN_COUNT - 1; + float smallTotal = smallW * static_cast(smallCount) + smallSpacing * static_cast(smallCount - 1); if (smallTotal > availableW) { float s = availableW / smallTotal; smallW *= s; @@ -45,14 +46,14 @@ std::array computeMenuButtonRects(const MenuLayoutParams& p) { std::array rects{}; rects[0] = SDL_FRect{ centerX - playW * 0.5f, playCY - playH * 0.5f, playW, playH }; - float rowW = smallW * 4.0f + smallSpacing * 3.0f; + float rowW = smallW * static_cast(smallCount) + smallSpacing * static_cast(smallCount - 1); float left = centerX - rowW * 0.5f; float minLeft = contentOffsetX + marginX; float maxRight = contentOffsetX + LOGICAL_W - marginX; if (left < minLeft) left = minLeft; if (left + rowW > maxRight) left = std::max(minLeft, maxRight - rowW); - for (int i = 0; i < 4; ++i) { + for (int i = 0; i < smallCount; ++i) { float x = left + i * (smallW + smallSpacing); rects[i + 1] = SDL_FRect{ x, smallCY - smallH * 0.5f, smallW, smallH }; } diff --git a/src/ui/UIConstants.h b/src/ui/UIConstants.h index cb9c89d..eb7b5c4 100644 --- a/src/ui/UIConstants.h +++ b/src/ui/UIConstants.h @@ -1,6 +1,6 @@ #pragma once -static constexpr int MENU_BTN_COUNT = 5; +static constexpr int MENU_BTN_COUNT = 6; static constexpr float MENU_SMALL_THRESHOLD = 700.0f; static constexpr float MENU_BTN_WIDTH_LARGE = 300.0f; static constexpr float MENU_BTN_WIDTH_SMALL_FACTOR = 0.4f; // multiplied by LOGICAL_W