new background image

This commit is contained in:
2025-12-18 07:20:20 +01:00
parent 0ab7121c5b
commit 989b98002c
11 changed files with 233 additions and 27 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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<double>(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<float>(std::max(std::max(std::max(optionsTransition, levelTransition), exitTransition), helpTransition));
float combinedTransition = static_cast<float>(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<float>(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<float>(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<float>(helpScroll);

View File

@ -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
};

View File

@ -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<Uint8>(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);

View File

@ -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,

View File

@ -5,7 +5,7 @@
namespace ui {
std::array<SDL_FRect, 5> computeMenuButtonRects(const MenuLayoutParams& p) {
std::array<SDL_FRect, MENU_BTN_COUNT> computeMenuButtonRects(const MenuLayoutParams& p) {
const float LOGICAL_W = static_cast<float>(p.logicalW);
const float LOGICAL_H = static_cast<float>(p.logicalH);
float contentOffsetX = (p.winW - LOGICAL_W * p.logicalScale) * 0.5f / p.logicalScale;
@ -13,7 +13,7 @@ std::array<SDL_FRect, 5> 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<SDL_FRect, 5> 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<float>(smallCount) + smallSpacing * static_cast<float>(smallCount - 1);
if (smallTotal > availableW) {
float s = availableW / smallTotal;
smallW *= s;
@ -45,14 +46,14 @@ std::array<SDL_FRect, 5> computeMenuButtonRects(const MenuLayoutParams& p) {
std::array<SDL_FRect, MENU_BTN_COUNT> 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<float>(smallCount) + smallSpacing * static_cast<float>(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 };
}

View File

@ -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