latest state

This commit is contained in:
2025-12-06 09:43:33 +01:00
parent 294e935344
commit b44de25113
19 changed files with 2451 additions and 524 deletions

View File

@ -12,10 +12,7 @@ void menu_updateFireworks(double frameMs);
double menu_getLogoAnimCounter();
int menu_getHoveredButton();
void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, bool isHovered, bool isSelected);
void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, SDL_Color bgColor, SDL_Color borderColor);
void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);
// Legacy wrappers removed
// void menu_drawEnhancedButton(...);
// void menu_drawMenuButton(...);
// void menu_drawSettingsPopup(...);

View File

@ -32,7 +32,7 @@ namespace Config {
constexpr int MAX_LEVELS = 20; // Maximum selectable starting level
// Gravity speed multiplier: 1.0 = normal, 2.0 = 2x slower, 0.5 = 2x faster
constexpr double GRAVITY_SPEED_MULTIPLIER = 1;
constexpr double GRAVITY_SPEED_MULTIPLIER = 2; // increase drop interval by ~100% to slow gravity
}
// UI Layout constants

View File

@ -20,6 +20,8 @@ void SpaceWarp::init(int w, int h, int starCount) {
for (auto& star : stars) {
respawn(star, true);
}
comets.clear();
cometSpawnTimer = randomRange(settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
}
void SpaceWarp::resize(int w, int h) {
@ -33,6 +35,7 @@ void SpaceWarp::resize(int w, int h) {
void SpaceWarp::setSettings(const SpaceWarpSettings& newSettings) {
settings = newSettings;
warpFactor = std::max(width, height) * settings.warpFactorScale;
cometSpawnTimer = std::clamp(cometSpawnTimer, settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
}
void SpaceWarp::setFlightMode(SpaceWarpFlightMode mode) {
@ -69,6 +72,7 @@ void SpaceWarp::setAutoPilotEnabled(bool enabled) {
flightMode = SpaceWarpFlightMode::Custom;
motionTarget = motion;
autoTimer = 0.0f;
scheduleNewAutoTarget();
}
}
@ -82,6 +86,32 @@ void SpaceWarp::scheduleNewAutoTarget() {
autoTimer = randomRange(autoMinInterval, autoMaxInterval);
}
void SpaceWarp::spawnComet() {
WarpComet comet;
float aspect = static_cast<float>(width) / static_cast<float>(std::max(1, height));
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);
comet.z = randomRange(minDepth + 4.0f, maxDepth);
float baseSpeed = randomRange(settings.minSpeed, settings.maxSpeed);
float multiplier = randomRange(settings.cometSpeedMultiplierMin, settings.cometSpeedMultiplierMax);
comet.speed = baseSpeed * multiplier;
comet.size = randomRange(settings.cometMinSize, settings.cometMaxSize);
comet.trailLength = randomRange(settings.cometMinTrail, settings.cometMaxTrail);
comet.life = randomRange(1.8f, 3.4f);
comet.maxLife = comet.life;
float shade = randomRange(0.85f, 1.0f);
Uint8 c = static_cast<Uint8>(std::clamp(220.0f + shade * 35.0f, 0.0f, 255.0f));
comet.color = SDL_Color{c, Uint8(std::min(255.0f, c * 0.95f)), 255, 255};
comet.prevScreenX = centerX;
comet.prevScreenY = centerY;
comet.screenX = centerX;
comet.screenY = centerY;
comets.push_back(comet);
}
float SpaceWarp::randomRange(float min, float max) {
std::uniform_real_distribution<float> dist(min, max);
return dist(rng);
@ -112,12 +142,16 @@ void SpaceWarp::respawn(WarpStar& star, bool randomDepth) {
}
bool SpaceWarp::project(const WarpStar& star, float& outX, float& outY) const {
if (star.z <= minDepth) {
return projectPoint(star.x, star.y, star.z, outX, outY);
}
bool SpaceWarp::projectPoint(float x, float y, float z, float& outX, float& outY) const {
if (z <= minDepth) {
return false;
}
float perspective = warpFactor / (star.z + 0.001f);
outX = centerX + star.x * perspective;
outY = centerY + star.y * perspective;
float perspective = warpFactor / (z + 0.001f);
outX = centerX + x * perspective;
outY = centerY + y * perspective;
const float margin = settings.spawnMargin;
return outX >= -margin && outX <= width + margin && outY >= -margin && outY <= height + margin;
}
@ -127,6 +161,14 @@ void SpaceWarp::update(float deltaSeconds) {
return;
}
if (settings.cometSpawnIntervalMax > 0.0f) {
cometSpawnTimer -= deltaSeconds;
if (cometSpawnTimer <= 0.0f) {
spawnComet();
cometSpawnTimer = randomRange(settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
}
}
if (autoPilotEnabled) {
autoTimer -= deltaSeconds;
if (autoTimer <= 0.0f) {
@ -188,6 +230,51 @@ void SpaceWarp::update(float deltaSeconds) {
star.prevScreenY = star.screenY - dy * scale;
}
}
for (auto it = comets.begin(); it != comets.end();) {
auto& comet = *it;
comet.life -= deltaSeconds;
comet.z -= comet.speed * deltaSeconds * forwardScale;
bool expired = comet.life <= 0.0f;
if (!movingBackward) {
if (comet.z <= minDepth * 0.35f) expired = true;
} else {
if (comet.z >= maxDepth + 40.0f) expired = true;
}
float closeness = 1.0f - std::clamp(comet.z / maxDepth, 0.0f, 1.0f);
float driftScale = (0.45f + closeness * 1.6f);
comet.x += lateralSpeed * deltaSeconds * driftScale;
comet.y += verticalSpeed * deltaSeconds * driftScale;
float sx = 0.0f;
float sy = 0.0f;
if (!projectPoint(comet.x, comet.y, comet.z, sx, sy)) {
expired = true;
} else {
comet.prevScreenX = comet.screenX;
comet.prevScreenY = comet.screenY;
comet.screenX = sx;
comet.screenY = sy;
float dx = comet.screenX - comet.prevScreenX;
float dy = comet.screenY - comet.prevScreenY;
float lenSq = dx * dx + dy * dy;
float maxTrail = std::max(comet.trailLength, 0.0f);
if (maxTrail > 0.0f && lenSq > maxTrail * maxTrail) {
float len = std::sqrt(lenSq);
float scale = maxTrail / len;
comet.prevScreenX = comet.screenX - dx * scale;
comet.prevScreenY = comet.screenY - dy * scale;
}
}
if (expired) {
it = comets.erase(it);
} else {
++it;
}
}
}
void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) {
@ -224,5 +311,16 @@ void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) {
SDL_RenderFillRect(renderer, &dot);
}
for (const auto& comet : comets) {
float lifeNorm = std::clamp(comet.life / comet.maxLife, 0.0f, 1.0f);
Uint8 alpha = static_cast<Uint8>(std::clamp(220.0f * lifeNorm, 0.0f, 255.0f));
SDL_SetRenderDrawColor(renderer, comet.color.r, comet.color.g, comet.color.b, alpha);
SDL_RenderLine(renderer, comet.prevScreenX, comet.prevScreenY, comet.screenX, comet.screenY);
float size = comet.size * (0.8f + (1.0f - lifeNorm) * 0.6f);
SDL_FRect head{comet.screenX - size * 0.5f, comet.screenY - size * 0.5f, size, size};
SDL_RenderFillRect(renderer, &head);
}
SDL_SetRenderDrawBlendMode(renderer, previous);
}

View File

@ -23,6 +23,14 @@ struct SpaceWarpSettings {
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
float cometSpawnIntervalMin = 2.8f; // minimum seconds between comet spawns
float cometSpawnIntervalMax = 6.5f; // maximum seconds between comet spawns
float cometSpeedMultiplierMin = 2.2f;// min multiplier for comet forward velocity
float cometSpeedMultiplierMax = 4.5f;// max multiplier for comet forward velocity
float cometMinTrail = 140.0f; // minimum comet trail length in pixels
float cometMaxTrail = 280.0f; // maximum comet trail length in pixels
float cometMinSize = 3.5f; // minimum comet head size
float cometMaxSize = 6.5f; // maximum comet head size
};
struct SpaceWarpFlightMotion {
@ -69,11 +77,30 @@ private:
Uint8 baseShade = 220;
};
struct WarpComet {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float speed = 0.0f;
float life = 0.0f;
float maxLife = 0.0f;
float prevScreenX = 0.0f;
float prevScreenY = 0.0f;
float screenX = 0.0f;
float screenY = 0.0f;
float trailLength = 160.0f;
float size = 4.0f;
SDL_Color color{255, 255, 255, 255};
};
void respawn(WarpStar& star, bool randomDepth = true);
bool project(const WarpStar& star, float& outX, float& outY) const;
bool projectPoint(float x, float y, float z, float& outX, float& outY) const;
float randomRange(float min, float max);
void spawnComet();
std::vector<WarpStar> stars;
std::vector<WarpComet> comets;
std::mt19937 rng;
int width = 0;
@ -90,6 +117,7 @@ private:
float autoMinInterval = 3.5f;
float autoMaxInterval = 7.5f;
SpaceWarpFlightMotion motionTarget{};
float cometSpawnTimer = 0.0f;
float minDepth = 2.0f;
float maxDepth = 320.0f;

View File

@ -0,0 +1,188 @@
#include "UIRenderer.h"
#include "../ui/Font.h"
#include <algorithm>
#include <cmath>
void UIRenderer::drawSciFiPanel(SDL_Renderer* renderer, const SDL_FRect& rect, float alpha) {
if (!renderer) return;
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
Uint8 alphaUint = static_cast<Uint8>(std::clamp(alpha * 255.0f, 0.0f, 255.0f));
// Drop shadow
SDL_FRect shadow{rect.x + 6.0f, rect.y + 10.0f, rect.w, rect.h};
SDL_SetRenderDrawColor(renderer, 0, 0, 0, static_cast<Uint8>(120.0f * alpha));
SDL_RenderFillRect(renderer, &shadow);
// Glow aura
for (int i = 0; i < 5; ++i) {
SDL_FRect glow{rect.x - float(i * 2), rect.y - float(i * 2), rect.w + float(i * 4), rect.h + float(i * 4)};
Uint8 glowAlpha = static_cast<Uint8>((42 - i * 8) * alpha);
SDL_SetRenderDrawColor(renderer, 0, 180, 255, glowAlpha);
SDL_RenderRect(renderer, &glow);
}
// Body
SDL_SetRenderDrawColor(renderer, 18, 30, 52, alphaUint);
SDL_RenderFillRect(renderer, &rect);
// Border
SDL_SetRenderDrawColor(renderer, 70, 120, 210, alphaUint);
SDL_RenderRect(renderer, &rect);
}
void UIRenderer::drawButton(SDL_Renderer* renderer, FontAtlas* font, float cx, float cy, float w, float h,
const std::string& label, bool isHovered, bool isSelected,
SDL_Color bgColor, SDL_Color borderColor, bool textOnly, SDL_Texture* icon) {
if (!renderer) return;
float x = cx - w * 0.5f;
float y = cy - h * 0.5f;
if (!textOnly) {
// Adjust colors based on state
if (isSelected) {
bgColor = {160, 190, 255, 255};
SDL_SetRenderDrawColor(renderer, 255, 220, 0, 110);
SDL_FRect glow{x - 10, y - 10, w + 20, h + 20};
SDL_RenderFillRect(renderer, &glow);
} else if (isHovered) {
bgColor = {static_cast<Uint8>(std::min(255, bgColor.r + 40)),
static_cast<Uint8>(std::min(255, bgColor.g + 40)),
static_cast<Uint8>(std::min(255, bgColor.b + 40)),
bgColor.a};
}
// Draw button background with border
SDL_SetRenderDrawColor(renderer, borderColor.r, borderColor.g, borderColor.b, borderColor.a);
SDL_FRect borderRect{x - 2, y - 2, w + 4, h + 4};
SDL_RenderFillRect(renderer, &borderRect);
SDL_SetRenderDrawColor(renderer, bgColor.r, bgColor.g, bgColor.b, bgColor.a);
SDL_FRect bgRect{x, y, w, h};
SDL_RenderFillRect(renderer, &bgRect);
}
// Draw icon if provided, otherwise draw text
if (icon) {
// Get icon dimensions
float iconW = 0.0f, iconH = 0.0f;
SDL_GetTextureSize(icon, &iconW, &iconH);
// Scale icon to fit nicely in button (60% of button height)
float maxIconH = h * 0.6f;
float scale = maxIconH / iconH;
float scaledW = iconW * scale;
float scaledH = iconH * scale;
// Center icon in button
float iconX = cx - scaledW * 0.5f;
float iconY = cy - scaledH * 0.5f;
// Apply yellow tint when selected
if (isSelected) {
SDL_SetTextureColorMod(icon, 255, 220, 0);
} else {
SDL_SetTextureColorMod(icon, 255, 255, 255);
}
SDL_FRect iconRect{iconX, iconY, scaledW, scaledH};
SDL_RenderTexture(renderer, icon, nullptr, &iconRect);
// Reset color mod
SDL_SetTextureColorMod(icon, 255, 255, 255);
} else if (font) {
// Draw text
float textScale = 1.5f;
int textW = 0, textH = 0;
font->measure(label, textScale, textW, textH);
float tx = x + (w - static_cast<float>(textW)) * 0.5f;
// Adjust vertical position for better alignment with background buttons
float ty = y + (h - static_cast<float>(textH)) * 0.5f + 2.0f;
// Choose text color based on selection state
SDL_Color textColor = {255, 255, 255, 255}; // Default white
if (isSelected) {
textColor = {255, 220, 0, 255}; // Yellow when selected
}
// Text shadow
font->draw(renderer, tx + 2.0f, ty + 2.0f, label, textScale, {0, 0, 0, 200});
// Text
font->draw(renderer, tx, ty, label, textScale, textColor);
}
}
void UIRenderer::computeContentOffsets(float winW, float winH, float logicalW, float logicalH, float logicalScale, float& outOffsetX, float& outOffsetY) {
float contentW = logicalW * logicalScale;
float contentH = logicalH * logicalScale;
outOffsetX = (winW - contentW) * 0.5f / logicalScale;
outOffsetY = (winH - contentH) * 0.5f / logicalScale;
}
void UIRenderer::drawLogo(SDL_Renderer* renderer, SDL_Texture* logoTex, float logicalW, float logicalH, float contentOffsetX, float contentOffsetY, int texW, int texH) {
if (!renderer || !logoTex) return;
float w = 0.0f;
float h = 0.0f;
if (texW > 0 && texH > 0) {
w = static_cast<float>(texW);
h = static_cast<float>(texH);
} else {
SDL_GetTextureSize(logoTex, &w, &h);
}
if (w > 0.0f && h > 0.0f) {
float maxWidth = logicalW * 0.6f;
float scale = std::min(1.0f, maxWidth / w);
float dw = w * scale;
float dh = h * scale;
float logoX = (logicalW - dw) * 0.5f + contentOffsetX;
float logoY = logicalH * 0.05f + contentOffsetY;
SDL_FRect dst{logoX, logoY, dw, dh};
SDL_RenderTexture(renderer, logoTex, nullptr, &dst);
}
}
void UIRenderer::drawSettingsPopup(SDL_Renderer* renderer, FontAtlas* font, float logicalW, float logicalH, bool musicEnabled, bool soundEnabled) {
if (!renderer || !font) return;
float popupW = 350, popupH = 260;
float popupX = (logicalW - popupW) / 2;
float popupY = (logicalH - popupH) / 2;
// Semi-transparent overlay
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 128);
SDL_FRect overlay{0, 0, logicalW, logicalH};
SDL_RenderFillRect(renderer, &overlay);
// Popup background
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);
// Title
font->draw(renderer, popupX + 20, popupY + 20, "SETTINGS", 2.0f, {255, 220, 0, 255});
// Music toggle
font->draw(renderer, popupX + 20, popupY + 70, "MUSIC:", 1.5f, {255, 255, 255, 255});
const char* musicStatus = musicEnabled ? "ON" : "OFF";
SDL_Color musicColor = musicEnabled ? SDL_Color{0, 255, 0, 255} : SDL_Color{255, 0, 0, 255};
font->draw(renderer, popupX + 120, popupY + 70, musicStatus, 1.5f, musicColor);
// Sound effects toggle
font->draw(renderer, popupX + 20, popupY + 100, "SOUND FX:", 1.5f, {255, 255, 255, 255});
const char* soundStatus = soundEnabled ? "ON" : "OFF";
SDL_Color soundColor = soundEnabled ? SDL_Color{0, 255, 0, 255} : SDL_Color{255, 0, 0, 255};
font->draw(renderer, popupX + 140, popupY + 100, soundStatus, 1.5f, soundColor);
// Instructions
font->draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, {200, 200, 220, 255});
font->draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, {200, 200, 220, 255});
font->draw(renderer, popupX + 20, popupY + 190, "ESC = CLOSE", 1.0f, {200, 200, 220, 255});
}

View File

@ -0,0 +1,28 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
class FontAtlas;
class UIRenderer {
public:
// Draw a sci-fi style panel with glow, shadow, and border
static void drawSciFiPanel(SDL_Renderer* renderer, const SDL_FRect& rect, float alpha = 1.0f);
// Draw a generic button with hover/select states
static void drawButton(SDL_Renderer* renderer, FontAtlas* font, float cx, float cy, float w, float h,
const std::string& label, bool isHovered, bool isSelected,
SDL_Color bgColor = {80, 110, 200, 255},
SDL_Color borderColor = {60, 80, 140, 255},
bool textOnly = false,
SDL_Texture* icon = nullptr);
// Helper to calculate content offsets for centering
static void computeContentOffsets(float winW, float winH, float logicalW, float logicalH, float logicalScale, float& outOffsetX, float& outOffsetY);
// Draw the game logo centered at the top
static void drawLogo(SDL_Renderer* renderer, SDL_Texture* logoTex, float logicalW, float logicalH, float contentOffsetX, float contentOffsetY, int texW = 0, int texH = 0);
// Draw the settings popup
static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas* font, float logicalW, float logicalH, bool musicEnabled, bool soundEnabled);
};

View File

@ -311,189 +311,7 @@ static void resetLevelBackgrounds(LevelBackgroundFader& fader) {
// ...existing code...
// -----------------------------------------------------------------------------
// Enhanced Button Drawing
// -----------------------------------------------------------------------------
static void drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, bool isHovered, bool isSelected = false) {
SDL_Color bgColor = isHovered ? SDL_Color{120, 150, 240, 255} : SDL_Color{80, 110, 200, 255};
if (isSelected) bgColor = {160, 190, 255, 255};
float x = cx - w/2;
float y = cy - h/2;
// Draw button background with border
drawRect(renderer, x-2, y-2, w+4, h+4, {60, 80, 140, 255}); // Border
drawRect(renderer, x, y, w, h, bgColor); // Background
// Draw button text centered
float textScale = 1.5f;
float textX = x + (w - label.length() * 12 * textScale) / 2;
float textY = y + (h - 20 * textScale) / 2;
font.draw(renderer, textX, textY, label, textScale, {255, 255, 255, 255});
}
// External wrapper for enhanced button so other translation units can call it.
void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, bool isHovered, bool isSelected) {
drawEnhancedButton(renderer, font, cx, cy, w, h, label, isHovered, isSelected);
}
// Popup wrappers
// Forward declarations for popup functions defined later in this file
static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);
void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) {
drawSettingsPopup(renderer, font, musicEnabled);
}
// Simple rounded menu button drawer used by MenuState (keeps visual parity with JS)
void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, SDL_Color bgColor, SDL_Color borderColor) {
float x = cx - w/2;
float y = cy - h/2;
drawRect(renderer, x-6, y-6, w+12, h+12, borderColor);
drawRect(renderer, x-4, y-4, w+8, h+8, {255,255,255,255});
drawRect(renderer, x, y, w, h, bgColor);
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(renderer, tx+2, ty+2, label, textScale, {0,0,0,180});
font.draw(renderer, tx, ty, label, textScale, {255,255,255,255});
}
// -----------------------------------------------------------------------------
// Block Drawing Functions
// -----------------------------------------------------------------------------
static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) {
if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) {
// Debug: print why we're falling back
if (!blocksTex) {
static bool printed = false;
if (!printed) {
(void)0;
printed = true;
}
}
// Fallback to colored rectangle if texture isn't available
SDL_Color color = (blockType >= 0 && blockType < PIECE_COUNT) ? COLORS[blockType + 1] : SDL_Color{128, 128, 128, 255};
drawRect(renderer, x, y, size-1, size-1, color);
return;
}
// JavaScript uses: sx = type * spriteSize, sy = 0, with 2px padding
// Each sprite is 90px wide in the horizontal sprite sheet
const int SPRITE_SIZE = 90;
float srcX = blockType * SPRITE_SIZE + 2; // Add 2px padding like JS
float srcY = 2; // Add 2px padding from top like JS
float srcW = SPRITE_SIZE - 4; // Subtract 4px total padding like JS
float srcH = SPRITE_SIZE - 4; // Subtract 4px total padding like JS
SDL_FRect srcRect = {srcX, srcY, srcW, srcH};
SDL_FRect dstRect = {x, y, size, size};
SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect);
}
static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false) {
if (piece.type >= PIECE_COUNT) return;
for (int cy = 0; cy < 4; ++cy) {
for (int cx = 0; cx < 4; ++cx) {
if (Game::cellFilled(piece, cx, cy)) {
float px = ox + (piece.x + cx) * tileSize;
float py = oy + (piece.y + cy) * tileSize;
if (isGhost) {
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
// Draw ghost piece as barely visible gray outline
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 20); // Very faint gray
SDL_FRect rect = {px + 2, py + 2, tileSize - 4, tileSize - 4};
SDL_RenderFillRect(renderer, &rect);
// Draw thin gray border
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 30);
SDL_FRect border = {px + 1, py + 1, tileSize - 2, tileSize - 2};
SDL_RenderRect(renderer, &border);
} else {
drawBlockTexture(renderer, blocksTex, px, py, tileSize, piece.type);
}
}
}
}
}
static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize) {
if (pieceType >= PIECE_COUNT) return;
// Use the first rotation (index 0) for preview
Game::Piece previewPiece;
previewPiece.type = pieceType;
previewPiece.rot = 0;
previewPiece.x = 0;
previewPiece.y = 0;
// Center the piece in the preview area
float offsetX = 0, offsetY = 0;
if (pieceType == I) { offsetX = tileSize * 0.5f; } // I-piece centering
else if (pieceType == O) { offsetX = tileSize * 0.5f; } // O-piece centering
// Use semi-transparent alpha for preview blocks
Uint8 previewAlpha = 180; // Change this value for more/less transparency
SDL_SetTextureAlphaMod(blocksTex, previewAlpha);
for (int cy = 0; cy < 4; ++cy) {
for (int cx = 0; cx < 4; ++cx) {
if (Game::cellFilled(previewPiece, cx, cy)) {
float px = x + offsetX + cx * tileSize;
float py = y + offsetY + cy * tileSize;
drawBlockTexture(renderer, blocksTex, px, py, tileSize, pieceType);
}
}
}
SDL_SetTextureAlphaMod(blocksTex, 255); // Reset alpha after drawing
}
// -----------------------------------------------------------------------------
// Popup Drawing Functions
// -----------------------------------------------------------------------------
static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) {
float popupW = 350, popupH = 260;
float popupX = (LOGICAL_W - popupW) / 2;
float popupY = (LOGICAL_H - popupH) / 2;
// Semi-transparent overlay
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 128);
SDL_FRect overlay{0, 0, LOGICAL_W, LOGICAL_H};
SDL_RenderFillRect(renderer, &overlay);
// Popup background
drawRect(renderer, popupX-4, popupY-4, popupW+8, popupH+8, {100, 120, 160, 255}); // Border
drawRect(renderer, popupX, popupY, popupW, popupH, {40, 50, 70, 255}); // Background
// Title
font.draw(renderer, popupX + 20, popupY + 20, "SETTINGS", 2.0f, {255, 220, 0, 255});
// Music toggle
font.draw(renderer, popupX + 20, popupY + 70, "MUSIC:", 1.5f, {255, 255, 255, 255});
const char* musicStatus = musicEnabled ? "ON" : "OFF";
SDL_Color musicColor = musicEnabled ? SDL_Color{0, 255, 0, 255} : SDL_Color{255, 0, 0, 255};
font.draw(renderer, popupX + 120, popupY + 70, musicStatus, 1.5f, musicColor);
// Sound effects toggle
font.draw(renderer, popupX + 20, popupY + 100, "SOUND FX:", 1.5f, {255, 255, 255, 255});
const char* soundStatus = SoundEffectManager::instance().isEnabled() ? "ON" : "OFF";
SDL_Color soundColor = SoundEffectManager::instance().isEnabled() ? SDL_Color{0, 255, 0, 255} : SDL_Color{255, 0, 0, 255};
font.draw(renderer, popupX + 140, popupY + 100, soundStatus, 1.5f, soundColor);
// Instructions
font.draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, {200, 200, 220, 255});
font.draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, {200, 200, 220, 255});
font.draw(renderer, popupX + 20, popupY + 190, "ESC = CLOSE", 1.0f, {200, 200, 220, 255});
}
// Legacy rendering functions removed (moved to UIRenderer / GameRenderer)
// -----------------------------------------------------------------------------
@ -521,7 +339,9 @@ static bool helpOverlayPausedGame = false;
// -----------------------------------------------------------------------------
// Tetris Block Fireworks for intro animation (block particles)
// Forward declare block render helper used by particles
static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType);
// Forward declare block render helper used by particles
// (Note: drawBlockTexture implementation was removed, so this is likely dead code unless particles use it.
// However, particles use drawFireworks_impl which uses SDL_RenderGeometry, so this is unused.)
// -----------------------------------------------------------------------------
struct BlockParticle {
float x{}, y{};
@ -791,11 +611,11 @@ int main(int, char **)
lineEffect.init(renderer);
// Load logo assets via SDL_image so we can use compressed formats
SDL_Texture* logoTex = loadTextureFromImage(renderer, "assets/images/logo.bmp");
SDL_Texture* logoTex = loadTextureFromImage(renderer, "assets/images/spacetris.png");
// Load small logo (used by Menu to show whole logo)
int logoSmallW = 0, logoSmallH = 0;
SDL_Texture* logoSmallTex = loadTextureFromImage(renderer, "assets/images/logo_small.bmp", &logoSmallW, &logoSmallH);
SDL_Texture* logoSmallTex = loadTextureFromImage(renderer, "assets/images/spacetris.png", &logoSmallW, &logoSmallH);
// Load menu background using SDL_image (prefers JPEG)
SDL_Texture* backgroundTex = loadTextureFromImage(renderer, "assets/images/main_background.bmp");

View File

@ -8,6 +8,7 @@
#include <algorithm>
#include <cstdio>
#include <string>
#include "../graphics/renderers/UIRenderer.h"
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
@ -93,30 +94,7 @@ static void Vignette(SDL_Renderer* r, int w, int h) {
FillRect(r, SDL_FRect{(float)w - pad, 0, (float)pad, (float)h}, SDL_Color{0, 0, 0, 140});
}
static SDL_FRect DrawPanel(SDL_Renderer* r, float w, float h, bool draw = true, float offX = 0.f, float offY = 0.f) {
float PW = std::min(520.f, w * 0.65f);
float PH = std::min(360.f, h * 0.7f);
SDL_FRect p{ (w - PW) / 2.f + offX, (h - PH) / 2.f - 40.f + offY, PW, PH }; // Moved up by 50px
if (!draw) return p; // geometry only
// drop shadow
FillRect(r, SDL_FRect{p.x + 6, p.y + 10, p.w, p.h}, SDL_Color{0, 0, 0, 120});
// glow aura
for (int i = 0; i < 6; i++) {
SDL_FRect g{ p.x - (float)(i * 2), p.y - (float)(i * 2), p.w + (float)(i * 4), p.h + (float)(i * 4) };
SDL_Color c = COL_CYAN_SO; c.a = (Uint8)(36 - i * 6);
StrokeRect(r, g, c);
}
// outer body + border
FillRect(r, p, COL_PANEL);
StrokeRect(r, p, COL_CYAN);
// inner face
FillRect(r, SDL_FRect{p.x + 12, p.y + 56, p.w - 24, p.h - 68}, COL_PANEL_IN);
StrokeRect(r, SDL_FRect{p.x + 12, p.y + 56, p.w - 24, p.h - 68}, SDL_Color{24, 31, 41, 180});
return p;
}
// DrawPanel removed, replaced by UIRenderer::drawSciFiPanel
struct Grid {
int cols = 4, rows = 5;
@ -195,7 +173,17 @@ void LevelSelectorState::handleEvent(const SDL_Event& e) {
float ly = (float(e.button.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f);
// Use same panel calculation as render (centered)
SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
const float LOGICAL_W_F = 1200.f;
const float LOGICAL_H_F = 1000.f;
float winW = (float)lastLogicalVP.w;
float winH = (float)lastLogicalVP.h;
float contentOffsetX = 0.0f;
float contentOffsetY = 0.0f;
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY);
float PW = std::min(520.f, LOGICAL_W_F * 0.65f);
float PH = std::min(360.f, LOGICAL_H_F * 0.7f);
SDL_FRect panel{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH };
Grid g = MakeGrid(panel);
int hit = HitTest(g, int(lx), int(ly));
if (hit != -1) {
@ -214,7 +202,17 @@ void LevelSelectorState::handleEvent(const SDL_Event& e) {
float ly = (float(e.motion.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f);
// Use same panel calculation as render (centered)
SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
const float LOGICAL_W_F = 1200.f;
const float LOGICAL_H_F = 1000.f;
float winW = (float)lastLogicalVP.w;
float winH = (float)lastLogicalVP.h;
float contentOffsetX = 0.0f;
float contentOffsetY = 0.0f;
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY);
float PW = std::min(520.f, LOGICAL_W_F * 0.65f);
float PH = std::min(360.f, LOGICAL_H_F * 0.7f);
SDL_FRect panel{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH };
Grid g = MakeGrid(panel);
hoveredLevel = HitTest(g, int(lx), int(ly));
}
@ -242,29 +240,30 @@ void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer, float l
// Compute content offsets (same approach as MenuState 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;
float contentOffsetX = 0.0f;
float contentOffsetY = 0.0f;
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY);
// Draw the logo at the top (same as MenuState)
SDL_Texture* logoToUse = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex;
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; // Match MenuState and OptionsState
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; // Match MenuState and OptionsState
SDL_FRect dst{logoX, logoY, dw, dh};
SDL_RenderTexture(renderer, logoToUse, nullptr, &dst);
int logoW = 0, logoH = 0;
if (logoToUse == ctx.logoSmallTex && ctx.logoSmallW > 0) {
logoW = ctx.logoSmallW;
logoH = ctx.logoSmallH;
}
UIRenderer::drawLogo(renderer, logoToUse, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY, logoW, logoH);
// Panel and title strip (in logical space) - centered properly with offsets
SDL_FRect panel = DrawPanel(renderer, LOGICAL_W, LOGICAL_H, /*draw=*/true, contentOffsetX, contentOffsetY);
float PW = std::min(520.f, LOGICAL_W * 0.65f);
float PH = std::min(360.f, LOGICAL_H * 0.7f);
SDL_FRect panel{ (LOGICAL_W - PW) / 2.f + contentOffsetX, (LOGICAL_H - PH) / 2.f - 40.f + contentOffsetY, PW, PH };
UIRenderer::drawSciFiPanel(renderer, panel);
// Inner face (LevelSelector specific)
SDL_FRect inner{panel.x + 12, panel.y + 56, panel.w - 24, panel.h - 68};
FillRect(renderer, inner, COL_PANEL_IN);
StrokeRect(renderer, inner, SDL_Color{24, 31, 41, 180});
// Title text - prefer pixelFont for a blocky title if available, fallback to regular font
FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
@ -296,7 +295,17 @@ bool LevelSelectorState::isMouseInPopup(float mouseX, float mouseY, float& popup
lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale;
ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale;
}
SDL_FRect p = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
const float LOGICAL_W_F = 1200.f;
const float LOGICAL_H_F = 1000.f;
float winW = (float)lastLogicalVP.w;
float winH = (float)lastLogicalVP.h;
float contentOffsetX = 0.0f;
float contentOffsetY = 0.0f;
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY);
float PW = std::min(520.f, LOGICAL_W_F * 0.65f);
float PH = std::min(360.f, LOGICAL_H_F * 0.7f);
SDL_FRect p{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH };
popupX = p.x; popupY = p.y; popupW = p.w; popupH = p.h;
return lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH;
}
@ -312,7 +321,17 @@ int LevelSelectorState::getLevelFromMouse(float mouseX, float mouseY, float popu
lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale;
ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale;
}
SDL_FRect p = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
const float LOGICAL_W_F = 1200.f;
const float LOGICAL_H_F = 1000.f;
float winW = (float)lastLogicalVP.w;
float winH = (float)lastLogicalVP.h;
float contentOffsetX = 0.0f;
float contentOffsetY = 0.0f;
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY);
float PW = std::min(520.f, LOGICAL_W_F * 0.65f);
float PH = std::min(360.f, LOGICAL_H_F * 0.7f);
SDL_FRect p{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH };
Grid g = MakeGrid(p);
return HitTest(g, (int)lx, (int)ly);
}

View File

@ -4,6 +4,7 @@
#include "../core/GlobalState.h"
#include "../core/state/StateManager.h"
#include "../audio/Audio.h"
#include "../audio/SoundEffect.h"
#include <SDL3/SDL.h>
#include <cstdio>
#include <algorithm>
@ -20,6 +21,8 @@
// Menu helper wrappers are declared in a shared header implemented in main.cpp
#include "../audio/MenuWrappers.h"
#include "../utils/ImagePathResolver.h"
#include "../graphics/renderers/UIRenderer.h"
#include "../graphics/renderers/GameRenderer.h"
#include <SDL3_image/SDL_image.h>
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
@ -38,6 +41,12 @@ void MenuState::onExit() {
if (ctx.showExitConfirmPopup) {
*ctx.showExitConfirmPopup = false;
}
// Clean up icon textures
if (playIcon) { SDL_DestroyTexture(playIcon); playIcon = nullptr; }
if (levelIcon) { SDL_DestroyTexture(levelIcon); levelIcon = nullptr; }
if (optionsIcon) { SDL_DestroyTexture(optionsIcon); optionsIcon = nullptr; }
if (exitIcon) { SDL_DestroyTexture(exitIcon); exitIcon = nullptr; }
}
void MenuState::handleEvent(const SDL_Event& e) {
@ -177,10 +186,9 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
// 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;
float contentOffsetX = 0.0f;
float contentOffsetY = 0.0f;
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY);
// Background is drawn by main (stretched to the full window) to avoid double-draw.
{
@ -250,106 +258,19 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
}
}
// 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;
static SDL_Texture* fallbackOverlay = nullptr;
static int fallbackW = 0;
static int fallbackH = 0;
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<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;
}
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<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);
}
}
// The main_screen overlay is drawn by main.cpp as the background
// We don't need to draw it again here as a logo
// Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test)
// Use the contentW calculated at the top with content offsets
float contentW = LOGICAL_W * logicalScale;
bool isSmall = (contentW < 700.0f);
float btnW = isSmall ? (LOGICAL_W * 0.32f) : (LOGICAL_W * 0.18f);
btnW = std::clamp(btnW, 180.0f, 260.0f); // keep buttons from consuming entire row
float btnH = isSmall ? 56.0f : 64.0f;
// Adjust button dimensions to match the background button graphics
float btnW = 200.0f; // Fixed width to match background buttons
float btnH = 70.0f; // Fixed height to match background buttons
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
// Adjust vertical position to align with background buttons
float btnY = LOGICAL_H * 0.865f + contentOffsetY;
if (ctx.pixelFont) {
{
@ -359,26 +280,6 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
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, bool selected){
float x = cx - w/2; float y = cy - h/2;
if (selected) {
SDL_SetRenderDrawColor(r, 255, 220, 0, 110);
SDL_FRect glow{ x-10, y-10, w+20, h+20 };
SDL_RenderFillRect(r, &glow);
}
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.5f;
int textW = 0, textH = 0;
font.measure(label, textScale, textW, textH);
float tx = x + (w - static_cast<float>(textW)) * 0.5f;
float ty = y + (h - static_cast<float>(textH)) * 0.5f;
font.draw(r, tx + 2.0f, ty + 2.0f, label, textScale, SDL_Color{0, 0, 0, 200});
font.draw(r, tx, ty, label, textScale, SDL_Color{255, 255, 255, 255});
};
struct MenuButtonDef {
SDL_Color bg;
SDL_Color border;
@ -391,137 +292,104 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
MenuButtonDef{ SDL_Color{130,80,210,255}, SDL_Color{90,40,170,255}, "OPTIONS" },
MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" }
};
// Icon array (nullptr if icon not loaded)
std::array<SDL_Texture*, 4> icons = {
playIcon,
levelIcon,
optionsIcon,
exitIcon
};
// Fixed spacing to match background button positions
float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f;
for (size_t i = 0; i < buttons.size(); ++i) {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
float cx = btnX + offset;
drawMenuButtonLocal(renderer, *ctx.pixelFont, cx, btnY, btnW, btnH, buttons[i].label, buttons[i].bg, buttons[i].border, selectedButton == static_cast<int>(i));
// Draw each button individually so each can have its own coordinates
// Button 0 - PLAY
{
const int i = 0;
float cxCenter = 0.0f;
float cyCenter = btnY;
if (ctx.menuButtonsExplicit) {
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
} else {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
cxCenter = btnX + offset;
}
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
}
// Button 1 - LEVEL
{
const int i = 1;
float cxCenter = 0.0f;
float cyCenter = btnY;
if (ctx.menuButtonsExplicit) {
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
} else {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
cxCenter = btnX + offset;
}
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
}
// Button 2 - OPTIONS
{
const int i = 2;
float cxCenter = 0.0f;
float cyCenter = btnY;
if (ctx.menuButtonsExplicit) {
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
} else {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
cxCenter = btnX + offset;
}
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
}
// Button 3 - EXIT
{
const int i = 3;
float cxCenter = 0.0f;
float cyCenter = btnY;
if (ctx.menuButtonsExplicit) {
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
} else {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
cxCenter = btnX + offset;
}
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
}
}
if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) {
int selection = ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
// Switch to window coordinates for full-screen overlay
SDL_SetRenderViewport(renderer, nullptr);
SDL_SetRenderScale(renderer, 1.0f, 1.0f);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 150);
// Get actual window size
int actualWinW = 0, actualWinH = 0;
SDL_GetRenderOutputSize(renderer, &actualWinW, &actualWinH);
SDL_FRect overlay{0, 0, (float)actualWinW, (float)actualWinH};
SDL_RenderFillRect(renderer, &overlay);
// Restore viewport and scale for popup content
SDL_SetRenderViewport(renderer, &logicalVP);
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
const float panelW = 640.0f;
const float panelH = 320.0f;
SDL_FRect panel{
(LOGICAL_W - panelW) * 0.5f + contentOffsetX,
(LOGICAL_H - panelH) * 0.5f + contentOffsetY,
panelW,
panelH
};
SDL_FRect shadow{panel.x + 6.0f, panel.y + 10.0f, panel.w, panel.h};
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140);
SDL_RenderFillRect(renderer, &shadow);
for (int i = 0; i < 5; ++i) {
SDL_FRect glow{panel.x - float(i * 2), panel.y - float(i * 2), panel.w + float(i * 4), panel.h + float(i * 4)};
SDL_SetRenderDrawColor(renderer, 0, 180, 255, Uint8(44 - i * 7));
SDL_RenderRect(renderer, &glow);
}
SDL_SetRenderDrawColor(renderer, 18, 30, 52, 255);
SDL_RenderFillRect(renderer, &panel);
SDL_SetRenderDrawColor(renderer, 70, 120, 210, 255);
SDL_RenderRect(renderer, &panel);
SDL_FRect inner{panel.x + 24.0f, panel.y + 98.0f, panel.w - 48.0f, panel.h - 146.0f};
SDL_SetRenderDrawColor(renderer, 16, 24, 40, 235);
SDL_RenderFillRect(renderer, &inner);
SDL_SetRenderDrawColor(renderer, 40, 80, 140, 235);
SDL_RenderRect(renderer, &inner);
FontAtlas* retroFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
if (retroFont) {
const float titleScale = 1.9f;
const char* title = "EXIT GAME?";
int titleW = 0, titleH = 0;
retroFont->measure(title, titleScale, titleW, titleH);
float titleX = panel.x + (panel.w - static_cast<float>(titleW)) * 0.5f;
retroFont->draw(renderer, titleX, panel.y + 30.0f, title, titleScale, SDL_Color{255, 230, 140, 255});
const float bodyScale = 1.05f;
const char* line = "Are you sure you want to quit?";
int bodyW = 0, bodyH = 0;
retroFont->measure(line, bodyScale, bodyW, bodyH);
float bodyX = panel.x + (panel.w - static_cast<float>(bodyW)) * 0.5f;
retroFont->draw(renderer, bodyX, inner.y + 18.0f, line, bodyScale, SDL_Color{210, 220, 240, 255});
}
const float horizontalPad = 28.0f;
const float buttonGap = 32.0f;
const float buttonH = 66.0f;
float buttonW = (inner.w - horizontalPad * 2.0f - buttonGap) * 0.5f;
float buttonY = inner.y + inner.h - buttonH - 24.0f;
auto drawChoice = [&](int idx, float x, const char* label) {
bool selected = (selection == idx);
SDL_Color base = (idx == 0) ? SDL_Color{185, 70, 70, 255} : SDL_Color{60, 95, 150, 255};
SDL_Color body = selected ? SDL_Color{Uint8(std::min(255, base.r + 35)), Uint8(std::min(255, base.g + 35)), Uint8(std::min(255, base.b + 35)), 255} : base;
SDL_Color border = selected ? SDL_Color{255, 220, 120, 255} : SDL_Color{80, 110, 160, 255};
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120);
SDL_FRect shadowRect{x + 4.0f, buttonY + 6.0f, buttonW, buttonH};
SDL_RenderFillRect(renderer, &shadowRect);
SDL_FRect bodyRect{x, buttonY, buttonW, buttonH};
SDL_SetRenderDrawColor(renderer, body.r, body.g, body.b, body.a);
SDL_RenderFillRect(renderer, &bodyRect);
SDL_SetRenderDrawColor(renderer, border.r, border.g, border.b, border.a);
SDL_RenderRect(renderer, &bodyRect);
if (retroFont) {
const float labelScale = 1.4f;
int textW = 0, textH = 0;
retroFont->measure(label, labelScale, textW, textH);
float textX = bodyRect.x + (bodyRect.w - static_cast<float>(textW)) * 0.5f;
float textY = bodyRect.y + (bodyRect.h - static_cast<float>(textH)) * 0.5f;
SDL_Color textColor = selected ? SDL_Color{255, 255, 255, 255} : SDL_Color{230, 235, 250, 255};
retroFont->draw(renderer, textX, textY, label, labelScale, textColor);
}
};
float yesX = inner.x + horizontalPad;
float noX = yesX + buttonW + buttonGap;
drawChoice(0, yesX, "YES");
drawChoice(1, noX, "NO");
GameRenderer::renderExitPopup(
renderer,
ctx.pixelFont,
winW,
winH,
logicalScale,
ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1
);
}
// 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, "ESC = CLOSE", 1.0f, SDL_Color{200,200,220,255});
bool soundOn = SoundEffectManager::instance().isEnabled();
UIRenderer::drawSettingsPopup(renderer, ctx.font, LOGICAL_W, LOGICAL_H, musicOn, soundOn);
}
// Trace exit
{

View File

@ -13,4 +13,10 @@ public:
private:
int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = EXIT
// Button icons (optional - will use text if nullptr)
SDL_Texture* playIcon = nullptr;
SDL_Texture* levelIcon = nullptr;
SDL_Texture* optionsIcon = nullptr;
SDL_Texture* exitIcon = nullptr;
};

View File

@ -7,6 +7,7 @@
#include <algorithm>
#include <cctype>
#include "../core/Settings.h"
#include "../graphics/renderers/UIRenderer.h"
OptionsState::OptionsState(StateContext& ctx) : State(ctx) {}
@ -89,32 +90,17 @@ void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
float winW = static_cast<float>(logicalVP.w);
float winH = static_cast<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;
float contentOffsetX = 0.0f;
float contentOffsetY = 0.0f;
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY);
SDL_Texture* logoTexture = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex;
if (logoTexture) {
float texW = 0.0f;
float texH = 0.0f;
if (logoTexture == ctx.logoSmallTex && ctx.logoSmallW > 0 && ctx.logoSmallH > 0) {
texW = static_cast<float>(ctx.logoSmallW);
texH = static_cast<float>(ctx.logoSmallH);
} else {
SDL_GetTextureSize(logoTexture, &texW, &texH);
}
if (texW > 0.0f && texH > 0.0f) {
float maxWidth = LOGICAL_W * 0.6f;
float scale = std::min(1.0f, maxWidth / texW);
float dw = texW * scale;
float dh = texH * scale;
float logoX = (LOGICAL_W - dw) * 0.5f + contentOffsetX;
float logoY = LOGICAL_H * 0.05f + contentOffsetY;
SDL_FRect dst{logoX, logoY, dw, dh};
SDL_RenderTexture(renderer, logoTexture, nullptr, &dst);
}
int logoW = 0, logoH = 0;
if (logoTexture == ctx.logoSmallTex && ctx.logoSmallW > 0) {
logoW = ctx.logoSmallW;
logoH = ctx.logoSmallH;
}
UIRenderer::drawLogo(renderer, logoTexture, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY, logoW, logoH);
const float panelW = 520.0f;
const float panelH = 420.0f;
@ -125,23 +111,7 @@ void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
panelH
};
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
// Panel styling similar to level selector
SDL_FRect shadow{panel.x + 6.0f, panel.y + 10.0f, panel.w, panel.h};
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120);
SDL_RenderFillRect(renderer, &shadow);
for (int i = 0; i < 5; ++i) {
SDL_FRect glow{panel.x - float(i * 2), panel.y - float(i * 2), panel.w + float(i * 4), panel.h + float(i * 4)};
SDL_SetRenderDrawColor(renderer, 0, 180, 255, Uint8(42 - i * 8));
SDL_RenderRect(renderer, &glow);
}
SDL_SetRenderDrawColor(renderer, 18, 30, 52, 255);
SDL_RenderFillRect(renderer, &panel);
SDL_SetRenderDrawColor(renderer, 70, 120, 210, 255);
SDL_RenderRect(renderer, &panel);
UIRenderer::drawSciFiPanel(renderer, panel);
FontAtlas* retroFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;

View File

@ -5,6 +5,7 @@
#include <vector>
#include <functional>
#include <string>
#include <array>
// Forward declarations for frequently used types
class Game;
@ -66,6 +67,12 @@ struct StateContext {
std::function<void(AppState)> requestFadeTransition; // Generic state fade requests (menu/options/level)
// Pointer to the application's StateManager so states can request transitions
StateManager* stateManager = nullptr;
// Optional explicit per-button coordinates (logical coordinates). When
// `menuButtonsExplicit` is true, MenuState will use these centers for
// rendering and hit tests. Values are in logical units (LOGICAL_W/H).
std::array<float, 4> menuButtonCX{};
std::array<float, 4> menuButtonCY{};
bool menuButtonsExplicit = false;
};
class State {