Add an in-game exit confirmation modal for Playing state: ESC opens modal and pauses the game; YES resets and returns to Menu; NO hides modal and resumes. Draw a full-window translucent dim background (reset viewport) so overlay covers any window size / fullscreen. Use PressStart2P (pixel P2) font for all modal text and center title/body/button labels using measured text widths. Add FontAtlas::measure(...) to accurately measure text sizes (used for proper centering). Ensure popup rendering and mouse hit-testing use the same logical/content-local coordinate math so visuals and clicks align. Add keyboard shortcuts for modal (Enter = confirm, Esc = cancel) and suppress other gameplay input while modal is active. Add helper scripts for debug build+run: build-debug-and-run.ps1 and build-debug-and-run.bat. Minor fixes to related rendering & state wiring; verified Debug build completes and modal behavior in runtime.
1767 lines
83 KiB
C++
1767 lines
83 KiB
C++
// main.cpp - Application orchestration (initialization, loop, UI states)
|
|
// High-level only: delegates Tetris logic, scores, background, font rendering.
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3/SDL_main.h>
|
|
#include <SDL3_ttf/SDL_ttf.h>
|
|
#include <string>
|
|
#include <cstdio>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <vector>
|
|
#include <random>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <memory>
|
|
|
|
#include "audio/Audio.h"
|
|
#include "audio/SoundEffect.h"
|
|
|
|
#include "gameplay/Game.h"
|
|
#include "persistence/Scores.h"
|
|
#include "graphics/Starfield.h"
|
|
#include "Starfield3D.h"
|
|
#include "graphics/Font.h"
|
|
#include "gameplay/LineEffect.h"
|
|
#include "states/State.h"
|
|
#include "states/LoadingState.h"
|
|
#include "states/MenuState.h"
|
|
#include "states/PlayingState.h"
|
|
#include "audio/MenuWrappers.h"
|
|
|
|
// Debug logging removed: no-op in this build (previously LOG_DEBUG)
|
|
|
|
// Font rendering now handled by FontAtlas
|
|
|
|
// ---------- Game config ----------
|
|
static constexpr int LOGICAL_W = 1200;
|
|
static constexpr int LOGICAL_H = 1000;
|
|
static constexpr int WELL_W = Game::COLS * Game::TILE;
|
|
static constexpr int WELL_H = Game::ROWS * Game::TILE;
|
|
|
|
// Piece types now declared in Game.h
|
|
|
|
// Scores now managed by ScoreManager
|
|
|
|
// 4x4 shapes encoded as 16-bit bitmasks per rotation (row-major 4x4).
|
|
// Bit 0 = (x=0,y=0), Bit 1 = (1,0) ... Bit 15 = (3,3)
|
|
// Shapes & game logic now in Game.cpp
|
|
|
|
// (removed inline shapes)
|
|
|
|
// Piece struct now in Game.h
|
|
|
|
// Game struct replaced by Game class
|
|
|
|
static const std::array<SDL_Color, PIECE_COUNT + 1> COLORS = {{
|
|
SDL_Color{20, 20, 26, 255}, // 0 empty
|
|
SDL_Color{0, 255, 255, 255}, // I
|
|
SDL_Color{255, 255, 0, 255}, // O
|
|
SDL_Color{160, 0, 255, 255}, // T
|
|
SDL_Color{0, 255, 0, 255}, // S
|
|
SDL_Color{255, 0, 0, 255}, // Z
|
|
SDL_Color{0, 0, 255, 255}, // J
|
|
SDL_Color{255, 160, 0, 255}, // L
|
|
}};
|
|
|
|
static void drawRect(SDL_Renderer *r, float x, float y, float w, float h, SDL_Color c)
|
|
{
|
|
SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a);
|
|
SDL_FRect fr{x, y, w, h};
|
|
SDL_RenderFillRect(r, &fr);
|
|
}
|
|
|
|
// ...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 drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel);
|
|
static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);
|
|
|
|
void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel) {
|
|
drawLevelSelectionPopup(renderer, font, bgTex, selectedLevel);
|
|
}
|
|
|
|
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 drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel) {
|
|
// Popup dims scale with logical size for responsiveness
|
|
float popupW = std::min(760.0f, LOGICAL_W * 0.75f);
|
|
float popupH = std::min(520.0f, LOGICAL_H * 0.7f);
|
|
float popupX = (LOGICAL_W - popupW) / 2.0f;
|
|
float popupY = (LOGICAL_H - popupH) / 2.0f;
|
|
|
|
// Draw the background picture stretched to full logical viewport if available
|
|
if (bgTex) {
|
|
// Dim the background by rendering it then overlaying a semi-transparent black rect
|
|
SDL_FRect dst{0, 0, (float)LOGICAL_W, (float)LOGICAL_H};
|
|
SDL_RenderTexture(renderer, bgTex, nullptr, &dst);
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 160);
|
|
SDL_FRect dim{0,0,(float)LOGICAL_W,(float)LOGICAL_H};
|
|
SDL_RenderFillRect(renderer, &dim);
|
|
} else {
|
|
// Fallback to semi-transparent overlay
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180);
|
|
SDL_FRect overlay{0, 0, (float)LOGICAL_W, (float)LOGICAL_H};
|
|
SDL_RenderFillRect(renderer, &overlay);
|
|
}
|
|
|
|
// Popup panel with border and subtle background
|
|
drawRect(renderer, popupX-6, popupY-6, popupW+12, popupH+12, {90, 110, 140, 200}); // outer border
|
|
drawRect(renderer, popupX-3, popupY-3, popupW+6, popupH+6, {30, 38, 60, 220}); // inner border
|
|
drawRect(renderer, popupX, popupY, popupW, popupH, {18, 22, 34, 235}); // panel
|
|
|
|
// Title (use retro pixel font)
|
|
font.draw(renderer, popupX + 28, popupY + 18, "SELECT STARTING LEVEL", 2.4f, {255, 220, 0, 255});
|
|
|
|
// Grid layout for levels: 4 columns x 5 rows
|
|
int cols = 4, rows = 5;
|
|
float padding = 24.0f;
|
|
float gridW = popupW - padding * 2;
|
|
float gridH = popupH - 120.0f; // leave space for title and instructions
|
|
float cellW = gridW / cols;
|
|
float cellH = std::min(80.0f, gridH / rows - 12.0f);
|
|
|
|
float gridStartX = popupX + padding;
|
|
float gridStartY = popupY + 70;
|
|
|
|
for (int level = 0; level < 20; ++level) {
|
|
int row = level / cols;
|
|
int col = level % cols;
|
|
float cx = gridStartX + col * cellW;
|
|
float cy = gridStartY + row * (cellH + 12.0f);
|
|
|
|
bool isSelected = (level == selectedLevel);
|
|
SDL_Color bg = isSelected ? SDL_Color{255, 220, 0, 255} : SDL_Color{70, 85, 120, 240};
|
|
SDL_Color fg = isSelected ? SDL_Color{0, 0, 0, 255} : SDL_Color{240, 240, 245, 255};
|
|
|
|
// Button background
|
|
drawRect(renderer, cx + 8, cy, cellW - 16, cellH, bg);
|
|
|
|
// Level label centered
|
|
char levelStr[8]; snprintf(levelStr, sizeof(levelStr), "%d", level);
|
|
float tx = cx + (cellW / 2.0f) - (6.0f * 1.8f); // rough centering
|
|
float ty = cy + (cellH / 2.0f) - 10.0f;
|
|
font.draw(renderer, tx, ty, levelStr, 1.8f, fg);
|
|
}
|
|
|
|
// Instructions under grid
|
|
font.draw(renderer, popupX + 28, popupY + popupH - 40, "CLICK A LEVEL TO SELECT • ESC = CANCEL", 1.0f, {200,200,220,255});
|
|
}
|
|
|
|
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, "N = PLAY LETS_GO", 1.0f, {200, 200, 220, 255});
|
|
font.draw(renderer, popupX + 20, popupY + 210, "ESC = CLOSE", 1.0f, {200, 200, 220, 255});
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Starfield effect for background
|
|
// -----------------------------------------------------------------------------
|
|
// Starfield now managed by Starfield class
|
|
|
|
// State manager integration (scaffolded in StateManager.h)
|
|
#include "core/StateManager.h"
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Intro/Menu state variables
|
|
// -----------------------------------------------------------------------------
|
|
static double logoAnimCounter = 0.0;
|
|
static bool showLevelPopup = false;
|
|
static bool showSettingsPopup = false;
|
|
static bool showExitConfirmPopup = false;
|
|
static bool musicEnabled = true;
|
|
static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// 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);
|
|
// -----------------------------------------------------------------------------
|
|
struct BlockParticle {
|
|
float x, y, vx, vy, size, alpha, decay;
|
|
int blockType; // 0..6
|
|
BlockParticle(float sx, float sy)
|
|
: x(sx), y(sy) {
|
|
float angle = (rand() % 628) / 100.0f; // 0..2pi
|
|
float speed = 1.5f + (rand() % 350) / 100.0f; // ~1.5..5.0
|
|
vx = std::cos(angle) * speed;
|
|
vy = std::sin(angle) * speed;
|
|
size = 6.0f + (rand() % 50) / 10.0f; // 6..11 px
|
|
alpha = 1.0f;
|
|
decay = 0.012f + (rand() % 200) / 10000.0f; // 0.012..0.032
|
|
blockType = rand() % 7; // choose a tetris color
|
|
}
|
|
bool update() {
|
|
vx *= 0.985f; // friction
|
|
vy = vy * 0.985f + 0.07f; // gravity
|
|
x += vx;
|
|
y += vy;
|
|
alpha -= decay;
|
|
size = std::max(2.0f, size - 0.04f);
|
|
return alpha > 0.02f;
|
|
}
|
|
};
|
|
|
|
struct TetrisFirework {
|
|
std::vector<BlockParticle> particles;
|
|
int mode = 0; // 0=random,1=red,2=green,3=palette
|
|
TetrisFirework(float x, float y) {
|
|
mode = rand() % 4;
|
|
int particleCount = 30 + rand() % 25; // 30-55 particles
|
|
particles.reserve(particleCount);
|
|
for (int i = 0; i < particleCount; ++i) particles.emplace_back(x, y);
|
|
}
|
|
bool update() {
|
|
for (auto it = particles.begin(); it != particles.end();) {
|
|
if (!it->update()) it = particles.erase(it); else ++it;
|
|
}
|
|
return !particles.empty();
|
|
}
|
|
// Drawing is handled by drawFireworks_impl which accepts the texture to use.
|
|
};
|
|
|
|
static std::vector<TetrisFirework> fireworks;
|
|
static Uint64 lastFireworkTime = 0;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Fireworks Management
|
|
// -----------------------------------------------------------------------------
|
|
static void updateFireworks(double frameMs) {
|
|
Uint64 now = SDL_GetTicks();
|
|
// Randomly spawn new block fireworks (2% chance per frame), bias to lower-right
|
|
if (fireworks.size() < 5 && (rand() % 100) < 2) {
|
|
float x = LOGICAL_W * 0.55f + float(rand() % int(LOGICAL_W * 0.35f));
|
|
float y = LOGICAL_H * 0.80f + float(rand() % int(LOGICAL_H * 0.15f));
|
|
fireworks.emplace_back(x, y);
|
|
lastFireworkTime = now;
|
|
}
|
|
|
|
// Update existing fireworks
|
|
for (auto it = fireworks.begin(); it != fireworks.end();) {
|
|
if (!it->update()) {
|
|
it = fireworks.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Primary implementation that accepts a texture pointer
|
|
static void drawFireworks_impl(SDL_Renderer* renderer, SDL_Texture* blocksTexture) {
|
|
for (auto& f : fireworks) {
|
|
// Particle draw uses the texture pointer passed into drawBlockTexture calls from f.draw
|
|
// We'll set a thread-local-ish variable by passing the texture as an argument to draw
|
|
// routines or using the provided texture in the particle's draw path.
|
|
// For simplicity, the particle draw function below will reference a global symbol
|
|
// via an argument — we adapt by providing the texture when calling drawBlockTexture.
|
|
// Implementation: call a small lambda that temporarily binds the texture for drawBlockTexture.
|
|
struct Drawer { SDL_Renderer* r; SDL_Texture* tex; void drawParticle(struct BlockParticle& p) {
|
|
if (tex) {
|
|
Uint8 prevA = 255;
|
|
SDL_GetTextureAlphaMod(tex, &prevA);
|
|
Uint8 setA = Uint8(std::max(0.0f, std::min(1.0f, p.alpha)) * 255.0f);
|
|
SDL_SetTextureAlphaMod(tex, setA);
|
|
// Note: color modulation will be applied by callers of drawBlockTexture where needed
|
|
// but we mimic behavior from previous implementation by leaving color mod as default.
|
|
drawBlockTexture(r, tex, p.x - p.size * 0.5f, p.y - p.size * 0.5f, p.size, p.blockType);
|
|
SDL_SetTextureAlphaMod(tex, prevA);
|
|
SDL_SetTextureColorMod(tex, 255, 255, 255);
|
|
} else {
|
|
SDL_SetRenderDrawColor(r, 255, 255, 255, Uint8(p.alpha * 255));
|
|
SDL_FRect rect{p.x - p.size/2, p.y - p.size/2, p.size, p.size};
|
|
SDL_RenderFillRect(r, &rect);
|
|
}
|
|
}
|
|
} drawer{renderer, blocksTexture};
|
|
for (auto &p : f.particles) {
|
|
drawer.drawParticle(p);
|
|
}
|
|
}
|
|
}
|
|
// External wrappers for use by other translation units (MenuState)
|
|
// Expect callers to pass the blocks texture via StateContext so we avoid globals.
|
|
void menu_drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex) { drawFireworks_impl(renderer, blocksTex); }
|
|
void menu_updateFireworks(double frameMs) { updateFireworks(frameMs); }
|
|
double menu_getLogoAnimCounter() { return logoAnimCounter; }
|
|
int menu_getHoveredButton() { return hoveredButton; }
|
|
|
|
int main(int, char **)
|
|
{
|
|
// Initialize random seed for fireworks
|
|
srand(static_cast<unsigned int>(SDL_GetTicks()));
|
|
|
|
int sdlInitRes = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
|
|
if (sdlInitRes < 0)
|
|
{
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed: %s", SDL_GetError());
|
|
return 1;
|
|
}
|
|
int ttfInitRes = TTF_Init();
|
|
if (ttfInitRes < 0)
|
|
{
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "TTF_Init failed");
|
|
SDL_Quit();
|
|
return 1;
|
|
}
|
|
SDL_Window *window = SDL_CreateWindow("Tetris (SDL3)", LOGICAL_W, LOGICAL_H, SDL_WINDOW_RESIZABLE);
|
|
if (!window)
|
|
{
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow failed: %s", SDL_GetError());
|
|
TTF_Quit();
|
|
SDL_Quit();
|
|
return 1;
|
|
}
|
|
SDL_Renderer *renderer = SDL_CreateRenderer(window, nullptr);
|
|
if (!renderer)
|
|
{
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateRenderer failed: %s", SDL_GetError());
|
|
SDL_DestroyWindow(window);
|
|
TTF_Quit();
|
|
SDL_Quit();
|
|
return 1;
|
|
}
|
|
SDL_SetRenderVSync(renderer, 1);
|
|
|
|
FontAtlas font;
|
|
font.init("FreeSans.ttf", 24);
|
|
|
|
// Load PressStart2P font for loading screen and retro UI elements
|
|
FontAtlas pixelFont;
|
|
pixelFont.init("assets/fonts/PressStart2P-Regular.ttf", 16);
|
|
|
|
ScoreManager scores;
|
|
scores.load();
|
|
Starfield starfield;
|
|
starfield.init(200, LOGICAL_W, LOGICAL_H);
|
|
Starfield3D starfield3D;
|
|
starfield3D.init(LOGICAL_W, LOGICAL_H, 200);
|
|
|
|
// Initialize line clearing effects
|
|
LineEffect lineEffect;
|
|
lineEffect.init(renderer);
|
|
|
|
// Load logo using native SDL BMP loading
|
|
SDL_Texture *logoTex = nullptr;
|
|
SDL_Surface* logoSurface = SDL_LoadBMP("assets/images/logo.bmp");
|
|
if (logoSurface) {
|
|
(void)0;
|
|
logoTex = SDL_CreateTextureFromSurface(renderer, logoSurface);
|
|
SDL_DestroySurface(logoSurface);
|
|
} else {
|
|
(void)0;
|
|
}
|
|
// Load small logo (used by Menu to show whole logo)
|
|
SDL_Texture *logoSmallTex = nullptr;
|
|
SDL_Surface* logoSmallSurface = SDL_LoadBMP("assets/images/logo_small.bmp");
|
|
int logoSmallW = 0, logoSmallH = 0;
|
|
if (logoSmallSurface) {
|
|
// capture surface size before creating the texture (avoids SDL_QueryTexture)
|
|
logoSmallW = logoSmallSurface->w;
|
|
logoSmallH = logoSmallSurface->h;
|
|
logoSmallTex = SDL_CreateTextureFromSurface(renderer, logoSmallSurface);
|
|
SDL_DestroySurface(logoSmallSurface);
|
|
} else {
|
|
// fallback: leave logoSmallTex null so MenuState will use large logo
|
|
(void)0;
|
|
}
|
|
|
|
// Load background using native SDL BMP loading
|
|
SDL_Texture *backgroundTex = nullptr;
|
|
SDL_Surface* backgroundSurface = SDL_LoadBMP("assets/images/main_background.bmp");
|
|
if (backgroundSurface) {
|
|
(void)0;
|
|
backgroundTex = SDL_CreateTextureFromSurface(renderer, backgroundSurface);
|
|
SDL_DestroySurface(backgroundSurface);
|
|
} else {
|
|
(void)0;
|
|
}
|
|
|
|
// Note: `backgroundTex` is owned by main and passed into `StateContext::backgroundTex` below.
|
|
// States should render using `ctx.backgroundTex` rather than accessing globals.
|
|
|
|
// Level background caching system
|
|
SDL_Texture *levelBackgroundTex = nullptr;
|
|
SDL_Texture *nextLevelBackgroundTex = nullptr; // used during fade transitions
|
|
int cachedLevel = -1; // Track which level background is currently cached
|
|
float levelFadeAlpha = 0.0f; // 0..1 blend factor where 1 means next fully visible
|
|
const float LEVEL_FADE_DURATION = 3500.0f; // ms for fade transition (3.5s)
|
|
float levelFadeElapsed = 0.0f;
|
|
|
|
// Load blocks texture using native SDL BMP loading
|
|
SDL_Texture *blocksTex = nullptr;
|
|
SDL_Surface* blocksSurface = SDL_LoadBMP("assets/images/blocks90px_001.bmp");
|
|
if (blocksSurface) {
|
|
(void)0;
|
|
blocksTex = SDL_CreateTextureFromSurface(renderer, blocksSurface);
|
|
SDL_DestroySurface(blocksSurface);
|
|
} else {
|
|
(void)0;
|
|
}
|
|
// No global exposure of blocksTex; states receive textures via StateContext.
|
|
|
|
if (!blocksTex) {
|
|
(void)0;
|
|
|
|
// Create a 630x90 texture (7 blocks * 90px each)
|
|
blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90);
|
|
if (blocksTex) {
|
|
// Set texture as render target and draw colored blocks
|
|
SDL_SetRenderTarget(renderer, blocksTex);
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
|
SDL_RenderClear(renderer);
|
|
|
|
// Draw each block type with its color
|
|
for (int i = 0; i < PIECE_COUNT; ++i) {
|
|
SDL_Color color = COLORS[i + 1];
|
|
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, 255);
|
|
SDL_FRect rect = {float(i * 90 + 4), 4, 82, 82}; // 4px padding, 82x82 block
|
|
SDL_RenderFillRect(renderer, &rect);
|
|
|
|
// Add a highlight effect
|
|
SDL_SetRenderDrawColor(renderer,
|
|
(Uint8)std::min(255, color.r + 30),
|
|
(Uint8)std::min(255, color.g + 30),
|
|
(Uint8)std::min(255, color.b + 30), 255);
|
|
SDL_FRect highlight = {float(i * 90 + 4), 4, 82, 20};
|
|
SDL_RenderFillRect(renderer, &highlight);
|
|
}
|
|
|
|
// Reset render target
|
|
SDL_SetRenderTarget(renderer, nullptr);
|
|
(void)0;
|
|
} else {
|
|
std::fprintf(stderr, "Failed to create programmatic texture: %s\n", SDL_GetError());
|
|
}
|
|
} else {
|
|
(void)0;
|
|
}
|
|
|
|
// Provide the blocks sheet to the fireworks system through StateContext (no globals).
|
|
|
|
// Default start level selection: 0
|
|
int startLevelSelection = 0;
|
|
Game game(startLevelSelection);
|
|
|
|
// Initialize sound effects system
|
|
SoundEffectManager::instance().init();
|
|
|
|
// Load sound effects
|
|
SoundEffectManager::instance().loadSound("clear_line", "assets/music/clear_line.wav");
|
|
|
|
// Load voice lines for line clears using WAV files (with MP3 fallback)
|
|
std::vector<std::string> doubleSounds = {"nice_combo", "you_fire", "well_played", "keep_that_ryhtm"};
|
|
std::vector<std::string> tripleSounds = {"great_move", "smooth_clear", "impressive", "triple_strike"};
|
|
std::vector<std::string> tetrisSounds = {"amazing", "you_re_unstoppable", "boom_tetris", "wonderful"};
|
|
|
|
// Helper function to load sound with WAV/MP3 fallback and file existence check
|
|
auto loadSoundWithFallback = [&](const std::string& id, const std::string& baseName) {
|
|
std::string wavPath = "assets/music/" + baseName + ".wav";
|
|
std::string mp3Path = "assets/music/" + baseName + ".mp3";
|
|
|
|
// Check if WAV file exists first
|
|
SDL_IOStream* wavFile = SDL_IOFromFile(wavPath.c_str(), "rb");
|
|
if (wavFile) {
|
|
SDL_CloseIO(wavFile);
|
|
if (SoundEffectManager::instance().loadSound(id, wavPath)) {
|
|
(void)0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fallback to MP3 if WAV doesn't exist or fails to load
|
|
SDL_IOStream* mp3File = SDL_IOFromFile(mp3Path.c_str(), "rb");
|
|
if (mp3File) {
|
|
SDL_CloseIO(mp3File);
|
|
if (SoundEffectManager::instance().loadSound(id, mp3Path)) {
|
|
(void)0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load sound: %s (tried both WAV and MP3)", id.c_str());
|
|
};
|
|
|
|
loadSoundWithFallback("nice_combo", "nice_combo");
|
|
loadSoundWithFallback("you_fire", "you_fire");
|
|
loadSoundWithFallback("well_played", "well_played");
|
|
loadSoundWithFallback("keep_that_ryhtm", "keep_that_ryhtm");
|
|
loadSoundWithFallback("great_move", "great_move");
|
|
loadSoundWithFallback("smooth_clear", "smooth_clear");
|
|
loadSoundWithFallback("impressive", "impressive");
|
|
loadSoundWithFallback("triple_strike", "triple_strike");
|
|
loadSoundWithFallback("amazing", "amazing");
|
|
loadSoundWithFallback("you_re_unstoppable", "you_re_unstoppable");
|
|
loadSoundWithFallback("boom_tetris", "boom_tetris");
|
|
loadSoundWithFallback("wonderful", "wonderful");
|
|
loadSoundWithFallback("lets_go", "lets_go"); // For level up
|
|
|
|
// Set up sound effect callbacks
|
|
game.setSoundCallback([&](int linesCleared) {
|
|
// Play basic line clear sound first
|
|
SoundEffectManager::instance().playSound("clear_line", 1.0f); // Increased volume
|
|
|
|
// Then play voice line based on number of lines cleared
|
|
if (linesCleared == 2) {
|
|
SoundEffectManager::instance().playRandomSound(doubleSounds, 1.0f); // Increased volume
|
|
} else if (linesCleared == 3) {
|
|
SoundEffectManager::instance().playRandomSound(tripleSounds, 1.0f); // Increased volume
|
|
} else if (linesCleared == 4) {
|
|
SoundEffectManager::instance().playRandomSound(tetrisSounds, 1.0f); // Increased volume
|
|
}
|
|
// Single line clears just play the basic clear sound (no voice in JS version)
|
|
});
|
|
|
|
game.setLevelUpCallback([&](int newLevel) {
|
|
// Play level up sound
|
|
SoundEffectManager::instance().playSound("lets_go", 1.0f); // Increased volume
|
|
});
|
|
|
|
AppState state = AppState::Loading;
|
|
double loadingProgress = 0.0;
|
|
Uint64 loadStart = SDL_GetTicks();
|
|
bool running = true, isFullscreen = false;
|
|
bool leftHeld = false, rightHeld = false;
|
|
double moveTimerMs = 0;
|
|
const double DAS = 170.0, ARR = 40.0;
|
|
SDL_Rect logicalVP{0, 0, LOGICAL_W, LOGICAL_H};
|
|
float logicalScale = 1.f;
|
|
Uint64 lastMs = SDL_GetTicks();
|
|
bool musicStarted = false;
|
|
bool musicLoaded = false;
|
|
int currentTrackLoading = 0;
|
|
int totalTracks = 0; // Will be set dynamically based on actual files
|
|
|
|
// Instantiate state manager
|
|
StateManager stateMgr(state);
|
|
|
|
// Prepare shared context for states
|
|
StateContext ctx{};
|
|
// Allow states to access the state manager for transitions
|
|
ctx.stateManager = &stateMgr;
|
|
ctx.game = &game;
|
|
ctx.scores = &scores;
|
|
ctx.starfield = &starfield;
|
|
ctx.starfield3D = &starfield3D;
|
|
ctx.font = &font;
|
|
ctx.pixelFont = &pixelFont;
|
|
ctx.lineEffect = &lineEffect;
|
|
ctx.logoTex = logoTex;
|
|
ctx.logoSmallTex = logoSmallTex;
|
|
ctx.logoSmallW = logoSmallW;
|
|
ctx.logoSmallH = logoSmallH;
|
|
ctx.backgroundTex = backgroundTex;
|
|
ctx.blocksTex = blocksTex;
|
|
ctx.musicEnabled = &musicEnabled;
|
|
ctx.startLevelSelection = &startLevelSelection;
|
|
ctx.hoveredButton = &hoveredButton;
|
|
ctx.showLevelPopup = &showLevelPopup;
|
|
ctx.showSettingsPopup = &showSettingsPopup;
|
|
ctx.showExitConfirmPopup = &showExitConfirmPopup;
|
|
|
|
// Instantiate state objects
|
|
auto loadingState = std::make_unique<LoadingState>(ctx);
|
|
auto menuState = std::make_unique<MenuState>(ctx);
|
|
auto playingState = std::make_unique<PlayingState>(ctx);
|
|
|
|
// Register handlers and lifecycle hooks
|
|
stateMgr.registerHandler(AppState::Loading, [&](const SDL_Event& e){ loadingState->handleEvent(e); });
|
|
stateMgr.registerOnEnter(AppState::Loading, [&](){ loadingState->onEnter(); });
|
|
stateMgr.registerOnExit(AppState::Loading, [&](){ loadingState->onExit(); });
|
|
|
|
stateMgr.registerHandler(AppState::Menu, [&](const SDL_Event& e){ menuState->handleEvent(e); });
|
|
stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); });
|
|
stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); });
|
|
|
|
// Combined Playing state handler: run playingState handler and inline gameplay mapping
|
|
stateMgr.registerHandler(AppState::Playing, [&](const SDL_Event& e){
|
|
// First give the PlayingState a chance to handle the event
|
|
playingState->handleEvent(e);
|
|
|
|
// Then perform inline gameplay mappings (gravity/rotation/hard-drop/hold)
|
|
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
|
if (!game.isPaused()) {
|
|
if (e.key.scancode == SDL_SCANCODE_SPACE) {
|
|
game.hardDrop();
|
|
}
|
|
else if (e.key.scancode == SDL_SCANCODE_UP) {
|
|
game.rotate(+1);
|
|
}
|
|
else if (e.key.scancode == SDL_SCANCODE_Z || (e.key.mod & SDL_KMOD_SHIFT)) {
|
|
game.rotate(-1);
|
|
}
|
|
else if (e.key.scancode == SDL_SCANCODE_C || (e.key.mod & SDL_KMOD_CTRL)) {
|
|
game.holdCurrent();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
stateMgr.registerOnEnter(AppState::Playing, [&](){ playingState->onEnter(); });
|
|
stateMgr.registerOnExit(AppState::Playing, [&](){ playingState->onExit(); });
|
|
|
|
// Playing, LevelSelect and GameOver currently use inline logic in main; we'll migrate later
|
|
while (running)
|
|
{
|
|
int winW = 0, winH = 0;
|
|
SDL_GetWindowSize(window, &winW, &winH);
|
|
|
|
// Use the full window for the viewport, scale to fit content
|
|
logicalScale = std::min(winW / (float)LOGICAL_W, winH / (float)LOGICAL_H);
|
|
if (logicalScale <= 0)
|
|
logicalScale = 1.f;
|
|
|
|
// Fill the entire window with our viewport
|
|
logicalVP.w = winW;
|
|
logicalVP.h = winH;
|
|
logicalVP.x = 0;
|
|
logicalVP.y = 0;
|
|
// --- Events ---
|
|
SDL_Event e;
|
|
while (SDL_PollEvent(&e))
|
|
{
|
|
if (e.type == SDL_EVENT_QUIT)
|
|
running = false;
|
|
else {
|
|
// Route event to state manager handlers for per-state logic
|
|
stateMgr.handleEvent(e);
|
|
|
|
// Global key toggles (applies regardless of state)
|
|
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
|
if (e.key.scancode == SDL_SCANCODE_M)
|
|
{
|
|
Audio::instance().toggleMute();
|
|
musicEnabled = !musicEnabled;
|
|
}
|
|
if (e.key.scancode == SDL_SCANCODE_S)
|
|
{
|
|
// Toggle sound effects
|
|
SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled());
|
|
}
|
|
if (e.key.scancode == SDL_SCANCODE_N)
|
|
{
|
|
// Test sound effects - play lets_go.wav specifically
|
|
SoundEffectManager::instance().playSound("lets_go", 1.0f);
|
|
}
|
|
if (e.key.key == SDLK_F11 || (e.key.key == SDLK_RETURN && (e.key.mod & SDL_KMOD_ALT)))
|
|
{
|
|
isFullscreen = !isFullscreen;
|
|
SDL_SetWindowFullscreen(window, isFullscreen ? SDL_WINDOW_FULLSCREEN : 0);
|
|
}
|
|
}
|
|
|
|
// Mouse handling remains in main loop for UI interactions
|
|
if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN)
|
|
{
|
|
float mx = (float)e.button.x, my = (float)e.button.y;
|
|
if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h)
|
|
{
|
|
float lx = (mx - logicalVP.x) / logicalScale, ly = (my - logicalVP.y) / logicalScale;
|
|
if (state == AppState::Menu)
|
|
{
|
|
// Compute content offsets (match MenuState centering)
|
|
float contentW = LOGICAL_W * logicalScale;
|
|
float contentH = LOGICAL_H * logicalScale;
|
|
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
|
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
|
|
|
if (showLevelPopup) {
|
|
// Handle level selection popup clicks
|
|
float popupW = 400, popupH = 300;
|
|
float popupX = (LOGICAL_W - popupW) / 2;
|
|
float popupY = (LOGICAL_H - popupH) / 2;
|
|
|
|
if (lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH) {
|
|
// Click inside popup - check level grid
|
|
float gridStartX = popupX + 50;
|
|
float gridStartY = popupY + 70;
|
|
float cellW = 70, cellH = 35;
|
|
|
|
if (lx >= gridStartX && ly >= gridStartY) {
|
|
int col = int((lx - gridStartX) / cellW);
|
|
int row = int((ly - gridStartY) / cellH);
|
|
if (col >= 0 && col < 4 && row >= 0 && row < 5) {
|
|
int selectedLevel = row * 4 + col;
|
|
if (selectedLevel < 20) {
|
|
startLevelSelection = selectedLevel;
|
|
showLevelPopup = false;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Click outside popup - close it
|
|
showLevelPopup = false;
|
|
}
|
|
} else if (showSettingsPopup) {
|
|
// Click anywhere closes settings popup
|
|
showSettingsPopup = false;
|
|
} else {
|
|
// Responsive Main menu buttons (match MenuState layout)
|
|
bool isSmall = ((LOGICAL_W * logicalScale) < 700.0f);
|
|
float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f;
|
|
float btnH = isSmall ? 60.0f : 70.0f;
|
|
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
|
const float btnYOffset = 40.0f; // must match MenuState offset
|
|
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
|
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
|
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
|
|
|
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h)
|
|
{
|
|
// Reset the game first with the chosen start level so HUD and
|
|
// Playing state see the correct 0-based level immediately.
|
|
game.reset(startLevelSelection);
|
|
state = AppState::Playing;
|
|
stateMgr.setState(state);
|
|
}
|
|
else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h)
|
|
{
|
|
showLevelPopup = true;
|
|
}
|
|
|
|
// Settings button (gear icon area - top right)
|
|
SDL_FRect settingsBtn{LOGICAL_W - 60, 10, 50, 30};
|
|
if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h)
|
|
{
|
|
showSettingsPopup = true;
|
|
}
|
|
}
|
|
}
|
|
else if (state == AppState::LevelSelect)
|
|
startLevelSelection = (startLevelSelection + 1) % 20;
|
|
else if (state == AppState::GameOver) {
|
|
state = AppState::Menu;
|
|
stateMgr.setState(state);
|
|
}
|
|
else if (state == AppState::Playing && showExitConfirmPopup) {
|
|
// Convert mouse to logical coordinates and to content-local coords
|
|
float lx = (mx - logicalVP.x) / logicalScale;
|
|
float ly = (my - logicalVP.y) / logicalScale;
|
|
// Compute content offsets (same as in render path)
|
|
float contentW = LOGICAL_W * logicalScale;
|
|
float contentH = LOGICAL_H * logicalScale;
|
|
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
|
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
|
// Map to content-local logical coords (what drawing code uses)
|
|
float localX = lx - contentOffsetX;
|
|
float localY = ly - contentOffsetY;
|
|
|
|
// Popup rect in logical coordinates (content-local)
|
|
float popupW = 420, popupH = 180;
|
|
float popupX = (LOGICAL_W - popupW) / 2;
|
|
float popupY = (LOGICAL_H - popupH) / 2;
|
|
|
|
if (localX >= popupX && localX <= popupX + popupW && localY >= popupY && localY <= popupY + popupH) {
|
|
// Inside popup: two buttons Yes / No
|
|
float btnW = 140, btnH = 46;
|
|
float yesX = popupX + popupW * 0.25f - btnW/2.0f;
|
|
float noX = popupX + popupW * 0.75f - btnW/2.0f;
|
|
float btnY = popupY + popupH - 60;
|
|
if (localX >= yesX && localX <= yesX + btnW && localY >= btnY && localY <= btnY + btnH) {
|
|
// Yes -> go back to menu
|
|
showExitConfirmPopup = false;
|
|
game.reset(startLevelSelection);
|
|
state = AppState::Menu;
|
|
stateMgr.setState(state);
|
|
}
|
|
else if (localX >= noX && localX <= noX + btnW && localY >= btnY && localY <= btnY + btnH) {
|
|
// No -> close popup and resume
|
|
showExitConfirmPopup = false;
|
|
game.setPaused(false);
|
|
}
|
|
} else {
|
|
// Click outside popup: cancel
|
|
showExitConfirmPopup = false;
|
|
game.setPaused(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (e.type == SDL_EVENT_MOUSE_MOTION)
|
|
{
|
|
float mx = (float)e.motion.x, my = (float)e.motion.y;
|
|
if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h)
|
|
{
|
|
float lx = (mx - logicalVP.x) / logicalScale, ly = (my - logicalVP.y) / logicalScale;
|
|
if (state == AppState::Menu && !showLevelPopup && !showSettingsPopup)
|
|
{
|
|
// Compute content offsets and responsive buttons (match MenuState)
|
|
float contentW = LOGICAL_W * logicalScale;
|
|
float contentH = LOGICAL_H * logicalScale;
|
|
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
|
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
|
bool isSmall = ((LOGICAL_W * logicalScale) < 700.0f);
|
|
float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f;
|
|
float btnH = isSmall ? 60.0f : 70.0f;
|
|
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
|
const float btnYOffset = 40.0f; // must match MenuState offset
|
|
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
|
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
|
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
|
|
|
|
hoveredButton = -1;
|
|
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h)
|
|
hoveredButton = 0;
|
|
else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h)
|
|
hoveredButton = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Timing ---
|
|
Uint64 now = SDL_GetTicks();
|
|
double frameMs = double(now - lastMs);
|
|
lastMs = now;
|
|
const bool *ks = SDL_GetKeyboardState(nullptr);
|
|
bool left = state == AppState::Playing && ks[SDL_SCANCODE_LEFT];
|
|
bool right = state == AppState::Playing && ks[SDL_SCANCODE_RIGHT];
|
|
bool down = state == AppState::Playing && ks[SDL_SCANCODE_DOWN];
|
|
|
|
// Inform game about soft-drop state for scoring parity (1 point per cell when holding Down)
|
|
if (state == AppState::Playing)
|
|
game.setSoftDropping(down && !game.isPaused());
|
|
else
|
|
game.setSoftDropping(false);
|
|
|
|
// Handle DAS/ARR
|
|
int moveDir = 0;
|
|
if (left && !right)
|
|
moveDir = -1;
|
|
else if (right && !left)
|
|
moveDir = +1;
|
|
|
|
if (moveDir != 0 && !game.isPaused())
|
|
{
|
|
if ((moveDir == -1 && leftHeld == false) || (moveDir == +1 && rightHeld == false))
|
|
{
|
|
game.move(moveDir);
|
|
moveTimerMs = DAS;
|
|
}
|
|
else
|
|
{
|
|
moveTimerMs -= frameMs;
|
|
if (moveTimerMs <= 0)
|
|
{
|
|
game.move(moveDir);
|
|
moveTimerMs += ARR;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
moveTimerMs = 0;
|
|
leftHeld = left;
|
|
rightHeld = right;
|
|
if (down && !game.isPaused())
|
|
game.softDropBoost(frameMs);
|
|
if (state == AppState::Playing)
|
|
{
|
|
if (!game.isPaused()) {
|
|
game.tickGravity(frameMs);
|
|
game.addElapsed(frameMs);
|
|
|
|
// Update line effect and clear lines when animation completes
|
|
if (lineEffect.isActive()) {
|
|
if (lineEffect.update(frameMs / 1000.0f)) {
|
|
// Effect is complete, now actually clear the lines
|
|
game.clearCompletedLines();
|
|
}
|
|
}
|
|
}
|
|
if (game.isGameOver())
|
|
{
|
|
scores.submit(game.score(), game.lines(), game.level(), game.elapsed());
|
|
state = AppState::GameOver;
|
|
stateMgr.setState(state);
|
|
}
|
|
}
|
|
else if (state == AppState::Loading)
|
|
{
|
|
// Initialize audio system and start background loading on first frame
|
|
if (!musicLoaded && currentTrackLoading == 0) {
|
|
Audio::instance().init();
|
|
|
|
// Count actual music files first
|
|
totalTracks = 0;
|
|
for (int i = 1; i <= 100; ++i) { // Check up to 100 files
|
|
char buf[64];
|
|
std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i);
|
|
|
|
// Check if file exists
|
|
SDL_IOStream* file = SDL_IOFromFile(buf, "rb");
|
|
if (file) {
|
|
SDL_CloseIO(file);
|
|
totalTracks++;
|
|
} else {
|
|
break; // No more consecutive files
|
|
}
|
|
}
|
|
|
|
// Add all found tracks to the background loading queue
|
|
for (int i = 1; i <= totalTracks; ++i) {
|
|
char buf[64];
|
|
std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i);
|
|
Audio::instance().addTrackAsync(buf);
|
|
}
|
|
|
|
// Start background loading thread
|
|
Audio::instance().startBackgroundLoading();
|
|
currentTrackLoading = 1; // Mark as started
|
|
}
|
|
|
|
// Update progress based on background loading
|
|
if (currentTrackLoading > 0 && !musicLoaded) {
|
|
currentTrackLoading = Audio::instance().getLoadedTrackCount();
|
|
if (Audio::instance().isLoadingComplete()) {
|
|
Audio::instance().shuffle(); // Shuffle once all tracks are loaded
|
|
musicLoaded = true;
|
|
}
|
|
}
|
|
|
|
// Calculate comprehensive loading progress
|
|
// Phase 1: Initial assets (textures, fonts) - 20%
|
|
double assetProgress = 0.2; // Assets are loaded at startup
|
|
|
|
// Phase 2: Music loading - 70%
|
|
double musicProgress = 0.0;
|
|
if (totalTracks > 0) {
|
|
musicProgress = musicLoaded ? 0.7 : std::min(0.7, (double)currentTrackLoading / totalTracks * 0.7);
|
|
}
|
|
|
|
// Phase 3: Final initialization - 10%
|
|
double timeProgress = std::min(0.1, (now - loadStart) / 500.0); // Faster final phase
|
|
|
|
loadingProgress = assetProgress + musicProgress + timeProgress;
|
|
|
|
// Ensure we never exceed 100% and reach exactly 100% when everything is loaded
|
|
loadingProgress = std::min(1.0, loadingProgress);
|
|
if (musicLoaded && timeProgress >= 0.1) {
|
|
loadingProgress = 1.0;
|
|
}
|
|
|
|
if (loadingProgress >= 1.0 && musicLoaded) {
|
|
state = AppState::Menu;
|
|
stateMgr.setState(state);
|
|
}
|
|
}
|
|
if (state == AppState::Menu || state == AppState::Playing)
|
|
{
|
|
if (!musicStarted && musicLoaded)
|
|
{
|
|
// Music tracks are already loaded during loading screen, just start playback
|
|
Audio::instance().start();
|
|
musicStarted = true;
|
|
}
|
|
}
|
|
|
|
// Update starfields based on current state
|
|
if (state == AppState::Loading) {
|
|
starfield3D.update(float(frameMs / 1000.0f));
|
|
starfield3D.resize(logicalVP.w, logicalVP.h); // Update for window resize
|
|
} else {
|
|
starfield.update(float(frameMs / 1000.0f), logicalVP.x * 2 + logicalVP.w, logicalVP.y * 2 + logicalVP.h);
|
|
}
|
|
|
|
// Advance level background fade if a next texture is queued
|
|
if (nextLevelBackgroundTex) {
|
|
levelFadeElapsed += float(frameMs);
|
|
levelFadeAlpha = std::min(1.0f, levelFadeElapsed / LEVEL_FADE_DURATION);
|
|
}
|
|
|
|
// Update intro animations
|
|
if (state == AppState::Menu) {
|
|
logoAnimCounter += frameMs * 0.0008; // Animation speed
|
|
updateFireworks(frameMs);
|
|
}
|
|
|
|
// --- Per-state update hooks (allow states to manage logic incrementally)
|
|
switch (stateMgr.getState()) {
|
|
case AppState::Loading:
|
|
loadingState->update(frameMs);
|
|
break;
|
|
case AppState::Menu:
|
|
menuState->update(frameMs);
|
|
break;
|
|
case AppState::Playing:
|
|
playingState->update(frameMs);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// --- Render ---
|
|
SDL_SetRenderViewport(renderer, nullptr);
|
|
SDL_SetRenderDrawColor(renderer, 12, 12, 16, 255);
|
|
SDL_RenderClear(renderer);
|
|
|
|
// Draw level-based background for gameplay, starfield for other states
|
|
if (state == AppState::Playing) {
|
|
// Use level-based background for gameplay with caching
|
|
int currentLevel = game.level();
|
|
int bgLevel = (currentLevel > 32) ? 32 : currentLevel; // Cap at level 32
|
|
|
|
// Only load new background if level changed
|
|
if (cachedLevel != bgLevel) {
|
|
// Load new level background into nextLevelBackgroundTex
|
|
if (nextLevelBackgroundTex) { SDL_DestroyTexture(nextLevelBackgroundTex); nextLevelBackgroundTex = nullptr; }
|
|
char bgPath[256];
|
|
snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.bmp", bgLevel);
|
|
SDL_Surface* levelBgSurface = SDL_LoadBMP(bgPath);
|
|
if (levelBgSurface) {
|
|
nextLevelBackgroundTex = SDL_CreateTextureFromSurface(renderer, levelBgSurface);
|
|
SDL_DestroySurface(levelBgSurface);
|
|
// start fade transition
|
|
levelFadeAlpha = 0.0f;
|
|
levelFadeElapsed = 0.0f;
|
|
cachedLevel = bgLevel;
|
|
} else {
|
|
// don't change textures if file missing
|
|
cachedLevel = -1;
|
|
}
|
|
}
|
|
|
|
// Draw blended backgrounds if needed
|
|
if (levelBackgroundTex || nextLevelBackgroundTex) {
|
|
SDL_FRect fullRect = { 0, 0, (float)logicalVP.w, (float)logicalVP.h };
|
|
// if fade in progress
|
|
if (nextLevelBackgroundTex && levelFadeAlpha < 1.0f && levelBackgroundTex) {
|
|
// draw current with inverse alpha
|
|
SDL_SetTextureAlphaMod(levelBackgroundTex, Uint8((1.0f - levelFadeAlpha) * 255));
|
|
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
|
|
SDL_SetTextureAlphaMod(nextLevelBackgroundTex, Uint8(levelFadeAlpha * 255));
|
|
SDL_RenderTexture(renderer, nextLevelBackgroundTex, nullptr, &fullRect);
|
|
// reset mods
|
|
SDL_SetTextureAlphaMod(levelBackgroundTex, 255);
|
|
SDL_SetTextureAlphaMod(nextLevelBackgroundTex, 255);
|
|
}
|
|
else if (nextLevelBackgroundTex && (!levelBackgroundTex || levelFadeAlpha >= 1.0f)) {
|
|
// finalise swap
|
|
if (levelBackgroundTex) { SDL_DestroyTexture(levelBackgroundTex); }
|
|
levelBackgroundTex = nextLevelBackgroundTex;
|
|
nextLevelBackgroundTex = nullptr;
|
|
levelFadeAlpha = 0.0f;
|
|
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
|
|
}
|
|
else if (levelBackgroundTex) {
|
|
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
|
|
}
|
|
}
|
|
} else if (state == AppState::Loading) {
|
|
// Use 3D starfield for loading screen (full screen)
|
|
starfield3D.draw(renderer);
|
|
} else if (state == AppState::Menu) {
|
|
// Use static background for menu, stretched to window; no starfield on sides
|
|
if (backgroundTex) {
|
|
SDL_FRect fullRect = { 0, 0, (float)logicalVP.w, (float)logicalVP.h };
|
|
SDL_RenderTexture(renderer, backgroundTex, nullptr, &fullRect);
|
|
}
|
|
} else {
|
|
// Use regular starfield for other states (not gameplay)
|
|
starfield.draw(renderer);
|
|
}
|
|
SDL_SetRenderViewport(renderer, &logicalVP);
|
|
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
|
|
|
|
switch (state)
|
|
{
|
|
case AppState::Loading:
|
|
{
|
|
// Calculate actual content area (centered within the window)
|
|
float contentScale = logicalScale;
|
|
float contentW = LOGICAL_W * contentScale;
|
|
float contentH = LOGICAL_H * contentScale;
|
|
float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
|
|
float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
|
|
|
|
auto drawRect = [&](float x, float y, float w, float h, SDL_Color c)
|
|
{ SDL_SetRenderDrawColor(renderer,c.r,c.g,c.b,c.a); SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h}; SDL_RenderFillRect(renderer,&fr); };
|
|
|
|
// Calculate dimensions for perfect centering (like JavaScript version)
|
|
const bool isLimitedHeight = LOGICAL_H < 450;
|
|
const float logoHeight = logoTex ? (isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f) : 0;
|
|
const float loadingTextHeight = 20; // Height of "LOADING" text (match JS)
|
|
const float barHeight = 20; // Loading bar height (match JS)
|
|
const float barPaddingVertical = isLimitedHeight ? 15 : 35;
|
|
const float percentTextHeight = 24; // Height of percentage text
|
|
const float spacingBetweenElements = isLimitedHeight ? 5 : 15;
|
|
|
|
// Total content height
|
|
const float totalContentHeight = logoHeight +
|
|
(logoHeight > 0 ? spacingBetweenElements : 0) +
|
|
loadingTextHeight +
|
|
barPaddingVertical +
|
|
barHeight +
|
|
spacingBetweenElements +
|
|
percentTextHeight;
|
|
|
|
// Start Y position for perfect vertical centering
|
|
float currentY = (LOGICAL_H - totalContentHeight) / 2.0f;
|
|
|
|
// Draw logo (centered, static like JavaScript version)
|
|
if (logoTex)
|
|
{
|
|
// Use the same original large logo dimensions as JS (we used a half-size BMP previously)
|
|
const int lw = 872, lh = 273;
|
|
|
|
// Cap logo width similar to JS UI.MAX_LOGO_WIDTH (600) and available screen space
|
|
const float maxLogoWidth = std::min(LOGICAL_W * 0.9f, 600.0f);
|
|
const float availableHeight = isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f;
|
|
const float availableWidth = maxLogoWidth;
|
|
|
|
const float scaleFactorWidth = availableWidth / static_cast<float>(lw);
|
|
const float scaleFactorHeight = availableHeight / static_cast<float>(lh);
|
|
const float scaleFactor = std::min(scaleFactorWidth, scaleFactorHeight);
|
|
|
|
const float displayWidth = lw * scaleFactor;
|
|
const float displayHeight = lh * scaleFactor;
|
|
const float logoX = (LOGICAL_W - displayWidth) / 2.0f;
|
|
|
|
SDL_FRect dst{logoX + contentOffsetX, currentY + contentOffsetY, displayWidth, displayHeight};
|
|
SDL_RenderTexture(renderer, logoTex, nullptr, &dst);
|
|
|
|
currentY += displayHeight + spacingBetweenElements;
|
|
}
|
|
|
|
// Draw "LOADING" text (centered, using pixel font)
|
|
const char* loadingText = "LOADING";
|
|
float textWidth = strlen(loadingText) * 12.0f; // Approximate width for pixel font
|
|
float textX = (LOGICAL_W - textWidth) / 2.0f;
|
|
pixelFont.draw(renderer, textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255, 204, 0, 255});
|
|
|
|
currentY += loadingTextHeight + barPaddingVertical;
|
|
|
|
// Draw loading bar (like JavaScript version)
|
|
const int barW = 400, barH = 20;
|
|
const int bx = (LOGICAL_W - barW) / 2;
|
|
|
|
// Bar border (dark gray) - using drawRect which adds content offset
|
|
drawRect(bx - 3, currentY - 3, barW + 6, barH + 6, {68, 68, 80, 255});
|
|
|
|
// Bar background (darker gray)
|
|
drawRect(bx, currentY, barW, barH, {34, 34, 34, 255});
|
|
|
|
// Progress bar (gold color)
|
|
drawRect(bx, currentY, int(barW * loadingProgress), barH, {255, 204, 0, 255});
|
|
|
|
currentY += barH + spacingBetweenElements;
|
|
|
|
// Draw percentage text (centered, using pixel font)
|
|
int percentage = int(loadingProgress * 100);
|
|
char percentText[16];
|
|
std::snprintf(percentText, sizeof(percentText), "%d%%", percentage);
|
|
|
|
float percentWidth = strlen(percentText) * 12.0f; // Approximate width for pixel font
|
|
float percentX = (LOGICAL_W - percentWidth) / 2.0f;
|
|
pixelFont.draw(renderer, percentX + contentOffsetX, currentY + contentOffsetY, percentText, 1.5f, {255, 204, 0, 255});
|
|
}
|
|
break;
|
|
case AppState::Menu:
|
|
// Delegate full menu rendering to MenuState object now
|
|
menuState->render(renderer, logicalScale, logicalVP);
|
|
break;
|
|
case AppState::LevelSelect:
|
|
font.draw(renderer, LOGICAL_W * 0.5f - 120, 80, "SELECT LEVEL", 2.5f, SDL_Color{255, 220, 0, 255});
|
|
{
|
|
char buf[64];
|
|
std::snprintf(buf, sizeof(buf), "LEVEL: %d", startLevelSelection);
|
|
font.draw(renderer, LOGICAL_W * 0.5f - 80, 180, buf, 2.0f, SDL_Color{200, 240, 255, 255});
|
|
}
|
|
font.draw(renderer, LOGICAL_W * 0.5f - 180, 260, "ARROWS CHANGE ENTER=OK ESC=BACK", 1.2f, SDL_Color{200, 200, 220, 255});
|
|
break;
|
|
case AppState::Playing:
|
|
{
|
|
// Calculate actual content area (centered within the window)
|
|
float contentScale = logicalScale;
|
|
float contentW = LOGICAL_W * contentScale;
|
|
float contentH = LOGICAL_H * contentScale;
|
|
float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
|
|
float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
|
|
|
|
// Draw the game with layout matching the JavaScript version
|
|
auto drawRect = [&](float x, float y, float w, float h, SDL_Color c)
|
|
{
|
|
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
|
|
SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h};
|
|
SDL_RenderFillRect(renderer, &fr);
|
|
};
|
|
|
|
// Responsive layout that scales with window size while maintaining margins
|
|
// Calculate available space considering UI panels and margins
|
|
const float MIN_MARGIN = 40.0f; // Minimum margin from edges
|
|
const float TOP_MARGIN = 60.0f; // Extra top margin for better spacing
|
|
const float PANEL_WIDTH = 180.0f; // Width of side panels
|
|
const float PANEL_SPACING = 30.0f; // Space between grid and panels
|
|
const float NEXT_PIECE_HEIGHT = 120.0f; // Space reserved for next piece preview (increased)
|
|
const float BOTTOM_MARGIN = 60.0f; // Space for controls text at bottom
|
|
|
|
// Available width = Total width - margins - left panel - right panel - spacing
|
|
const float availableWidth = LOGICAL_W - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2);
|
|
const float availableHeight = LOGICAL_H - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PIECE_HEIGHT;
|
|
|
|
// Calculate block size based on available space (maintain 10:20 aspect ratio)
|
|
const float maxBlockSizeW = availableWidth / Game::COLS;
|
|
const float maxBlockSizeH = availableHeight / Game::ROWS;
|
|
const float BLOCK_SIZE = std::min(maxBlockSizeW, maxBlockSizeH);
|
|
|
|
// Ensure minimum and maximum block sizes
|
|
const float finalBlockSize = std::max(20.0f, std::min(BLOCK_SIZE, 40.0f));
|
|
|
|
const float GRID_W = Game::COLS * finalBlockSize;
|
|
const float GRID_H = Game::ROWS * finalBlockSize;
|
|
|
|
// Calculate vertical position with proper top margin
|
|
const float totalContentHeight = NEXT_PIECE_HEIGHT + GRID_H;
|
|
const float availableVerticalSpace = LOGICAL_H - TOP_MARGIN - BOTTOM_MARGIN;
|
|
const float verticalCenterOffset = (availableVerticalSpace - totalContentHeight) * 0.5f;
|
|
const float contentStartY = TOP_MARGIN + verticalCenterOffset;
|
|
|
|
// Perfect horizontal centering - center the entire layout (grid + panels) in the window
|
|
const float totalLayoutWidth = PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + PANEL_WIDTH;
|
|
const float layoutStartX = (LOGICAL_W - totalLayoutWidth) * 0.5f;
|
|
|
|
// Calculate panel and grid positions from the centered layout
|
|
const float statsX = layoutStartX + contentOffsetX;
|
|
const float gridX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + contentOffsetX;
|
|
const float scoreX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + contentOffsetX;
|
|
|
|
// Position grid with proper top spacing
|
|
const float gridY = contentStartY + NEXT_PIECE_HEIGHT + contentOffsetY;
|
|
|
|
// Panel dimensions and positions
|
|
const float statsY = gridY;
|
|
const float statsW = PANEL_WIDTH;
|
|
const float statsH = GRID_H;
|
|
|
|
const float scoreY = gridY;
|
|
const float scoreW = PANEL_WIDTH;
|
|
|
|
// Next piece preview (above grid, centered)
|
|
const float nextW = finalBlockSize * 4 + 20;
|
|
const float nextH = finalBlockSize * 2 + 20;
|
|
const float nextX = gridX + (GRID_W - nextW) * 0.5f;
|
|
const float nextY = contentStartY + contentOffsetY;
|
|
|
|
// Handle line clearing effects (now that we have grid coordinates)
|
|
if (game.hasCompletedLines() && !lineEffect.isActive()) {
|
|
auto completedLines = game.getCompletedLines();
|
|
lineEffect.startLineClear(completedLines, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
|
}
|
|
|
|
// Draw panels with borders (like JS version)
|
|
|
|
// Game grid border
|
|
drawRect(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255}); // Outer border
|
|
drawRect(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 255}); // Inner border
|
|
drawRect(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255}); // Background
|
|
|
|
// Left panel background (BLOCKS panel) - translucent, slightly shorter height
|
|
{
|
|
SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160);
|
|
SDL_FRect lbg{statsX - 16, gridY - 10, statsW + 32, GRID_H + 20};
|
|
SDL_RenderFillRect(renderer, &lbg);
|
|
}
|
|
// Right panel background (SCORE/LINES/LEVEL etc) - translucent
|
|
{
|
|
SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160);
|
|
SDL_FRect rbg{scoreX - 16, gridY - 16, scoreW + 32, GRID_H + 32};
|
|
SDL_RenderFillRect(renderer, &rbg);
|
|
}
|
|
|
|
// Draw grid lines (subtle lines to show cell boundaries)
|
|
SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255); // Slightly lighter than background
|
|
|
|
// Vertical grid lines
|
|
for (int x = 1; x < Game::COLS; ++x) {
|
|
float lineX = gridX + x * finalBlockSize; // Remove duplicate contentOffsetX
|
|
SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H);
|
|
}
|
|
|
|
// Horizontal grid lines
|
|
for (int y = 1; y < Game::ROWS; ++y) {
|
|
float lineY = gridY + y * finalBlockSize; // Remove duplicate contentOffsetY
|
|
SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY);
|
|
}
|
|
|
|
// Block statistics panel
|
|
drawRect(statsX - 3 - contentOffsetX, statsY - 3 - contentOffsetY, statsW + 6, statsH + 6, {100, 120, 200, 255});
|
|
drawRect(statsX - contentOffsetX, statsY - contentOffsetY, statsW, statsH, {30, 35, 50, 255});
|
|
|
|
// Next piece preview panel
|
|
drawRect(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255});
|
|
drawRect(nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH, {30, 35, 50, 255});
|
|
|
|
// Draw the game board
|
|
const auto &board = game.boardRef();
|
|
for (int y = 0; y < Game::ROWS; ++y)
|
|
{
|
|
for (int x = 0; x < Game::COLS; ++x)
|
|
{
|
|
int v = board[y * Game::COLS + x];
|
|
if (v > 0) {
|
|
float bx = gridX + x * finalBlockSize;
|
|
float by = gridY + y * finalBlockSize;
|
|
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw ghost piece (where current piece will land)
|
|
if (!game.isPaused()) {
|
|
Game::Piece ghostPiece = game.current();
|
|
// Find landing position
|
|
while (true) {
|
|
Game::Piece testPiece = ghostPiece;
|
|
testPiece.y++;
|
|
bool collision = false;
|
|
|
|
// Simple collision check
|
|
for (int cy = 0; cy < 4; ++cy) {
|
|
for (int cx = 0; cx < 4; ++cx) {
|
|
if (Game::cellFilled(testPiece, cx, cy)) {
|
|
int gx = testPiece.x + cx;
|
|
int gy = testPiece.y + cy;
|
|
if (gy >= Game::ROWS || gx < 0 || gx >= Game::COLS ||
|
|
(gy >= 0 && board[gy * Game::COLS + gx] != 0)) {
|
|
collision = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (collision) break;
|
|
}
|
|
|
|
if (collision) break;
|
|
ghostPiece = testPiece;
|
|
}
|
|
|
|
// Draw ghost piece
|
|
drawPiece(renderer, blocksTex, ghostPiece, gridX, gridY, finalBlockSize, true);
|
|
}
|
|
|
|
// Draw the falling piece
|
|
if (!game.isPaused()) {
|
|
drawPiece(renderer, blocksTex, game.current(), gridX, gridY, finalBlockSize, false);
|
|
}
|
|
|
|
// Draw line clearing effects
|
|
if (lineEffect.isActive()) {
|
|
lineEffect.render(renderer, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
|
}
|
|
|
|
// Draw next piece preview
|
|
pixelFont.draw(renderer, nextX + 10, nextY - 20, "NEXT", 1.0f, {255, 220, 0, 255});
|
|
if (game.next().type < PIECE_COUNT) {
|
|
drawSmallPiece(renderer, blocksTex, game.next().type, nextX + 10, nextY + 10, finalBlockSize * 0.6f);
|
|
}
|
|
|
|
// Draw block statistics (left panel)
|
|
pixelFont.draw(renderer, statsX + 10, statsY + 10, "BLOCKS", 1.0f, {255, 220, 0, 255});
|
|
|
|
const auto& blockCounts = game.getBlockCounts();
|
|
int totalBlocks = 0; for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i];
|
|
const char* pieceNames[] = {"I", "O", "T", "S", "Z", "J", "L"};
|
|
// Dynamic vertical cursor so bars sit below blocks cleanly
|
|
float yCursor = statsY + 52;
|
|
for (int i = 0; i < PIECE_COUNT; ++i) {
|
|
// Baseline for this entry
|
|
float py = yCursor;
|
|
|
|
// Draw small piece icon (top of entry)
|
|
float previewSize = finalBlockSize * 0.55f;
|
|
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(i), statsX + 18, py, previewSize);
|
|
|
|
// Compute preview height in tiles (rotation 0)
|
|
int maxCy = -1;
|
|
{
|
|
Game::Piece prev; prev.type = static_cast<PieceType>(i); prev.rot = 0; prev.x = 0; prev.y = 0;
|
|
for (int cy = 0; cy < 4; ++cy) {
|
|
for (int cx = 0; cx < 4; ++cx) {
|
|
if (Game::cellFilled(prev, cx, cy)) maxCy = std::max(maxCy, cy);
|
|
}
|
|
}
|
|
}
|
|
int tilesHigh = (maxCy >= 0 ? maxCy + 1 : 1);
|
|
float previewHeight = tilesHigh * previewSize;
|
|
|
|
// Count on the right, near the top (aligned with blocks)
|
|
int count = blockCounts[i];
|
|
char countStr[16];
|
|
snprintf(countStr, sizeof(countStr), "%d", count);
|
|
pixelFont.draw(renderer, statsX + statsW - 20, py + 6, countStr, 1.1f, {240, 240, 245, 255});
|
|
|
|
// Percentage and bar BELOW the blocks
|
|
int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(count) / double(totalBlocks))) : 0;
|
|
char percStr[16];
|
|
snprintf(percStr, sizeof(percStr), "%d%%", perc);
|
|
|
|
float barX = statsX + 12;
|
|
float barY = py + previewHeight + 18.0f;
|
|
float barW = statsW - 24;
|
|
float barH = 6;
|
|
|
|
// Percent text just above the bar (left)
|
|
pixelFont.draw(renderer, barX, barY - 16, percStr, 0.8f, {230, 230, 235, 255});
|
|
|
|
// Track
|
|
SDL_SetRenderDrawColor(renderer, 170, 170, 175, 200);
|
|
SDL_FRect track{barX, barY, barW, barH};
|
|
SDL_RenderFillRect(renderer, &track);
|
|
// Fill (piece color)
|
|
SDL_Color pc = COLORS[i + 1];
|
|
SDL_SetRenderDrawColor(renderer, pc.r, pc.g, pc.b, 230);
|
|
float fillW = barW * (perc / 100.0f);
|
|
if (fillW < 0) fillW = 0; if (fillW > barW) fillW = barW;
|
|
SDL_FRect fill{barX, barY, fillW, barH};
|
|
SDL_RenderFillRect(renderer, &fill);
|
|
|
|
// Advance cursor: bar bottom + spacing
|
|
yCursor = barY + barH + 18.0f;
|
|
}
|
|
|
|
// Draw score panel (right side), centered vertically in grid
|
|
// Compute content vertical centering based on known offsets
|
|
const float contentTopOffset = 0.0f;
|
|
const float contentBottomOffset = 290.0f; // last line (time value)
|
|
const float contentPad = 36.0f;
|
|
float scoreContentH = (contentBottomOffset - contentTopOffset) + contentPad;
|
|
float baseY = gridY + (GRID_H - scoreContentH) * 0.5f;
|
|
|
|
pixelFont.draw(renderer, scoreX, baseY + 0, "SCORE", 1.0f, {255, 220, 0, 255});
|
|
char scoreStr[32];
|
|
snprintf(scoreStr, sizeof(scoreStr), "%d", game.score());
|
|
pixelFont.draw(renderer, scoreX, baseY + 25, scoreStr, 0.9f, {255, 255, 255, 255});
|
|
|
|
pixelFont.draw(renderer, scoreX, baseY + 70, "LINES", 1.0f, {255, 220, 0, 255});
|
|
char linesStr[16];
|
|
snprintf(linesStr, sizeof(linesStr), "%03d", game.lines());
|
|
pixelFont.draw(renderer, scoreX, baseY + 95, linesStr, 0.9f, {255, 255, 255, 255});
|
|
|
|
pixelFont.draw(renderer, scoreX, baseY + 140, "LEVEL", 1.0f, {255, 220, 0, 255});
|
|
char levelStr[16];
|
|
snprintf(levelStr, sizeof(levelStr), "%02d", game.level());
|
|
pixelFont.draw(renderer, scoreX, baseY + 165, levelStr, 0.9f, {255, 255, 255, 255});
|
|
|
|
// Next level progress
|
|
// JS rules: first threshold = (startLevel+1)*10; afterwards every +10
|
|
int startLv = game.startLevelBase(); // 0-based
|
|
int firstThreshold = (startLv + 1) * 10;
|
|
int linesDone = game.lines();
|
|
int nextThreshold = 0;
|
|
if (linesDone < firstThreshold) {
|
|
nextThreshold = firstThreshold;
|
|
} else {
|
|
int blocksPast = linesDone - firstThreshold;
|
|
nextThreshold = firstThreshold + ((blocksPast / 10) + 1) * 10;
|
|
}
|
|
int linesForNext = std::max(0, nextThreshold - linesDone);
|
|
pixelFont.draw(renderer, scoreX, baseY + 200, "NEXT LVL", 1.0f, {255, 220, 0, 255});
|
|
char nextStr[32];
|
|
snprintf(nextStr, sizeof(nextStr), "%d LINES", linesForNext);
|
|
pixelFont.draw(renderer, scoreX, baseY + 225, nextStr, 0.9f, {80, 255, 120, 255});
|
|
|
|
// Time
|
|
pixelFont.draw(renderer, scoreX, baseY + 265, "TIME", 1.0f, {255, 220, 0, 255});
|
|
int totalSecs = static_cast<int>(game.elapsed());
|
|
int mins = totalSecs / 60;
|
|
int secs = totalSecs % 60;
|
|
char timeStr[16];
|
|
snprintf(timeStr, sizeof(timeStr), "%02d:%02d", mins, secs);
|
|
pixelFont.draw(renderer, scoreX, baseY + 290, timeStr, 0.9f, {255, 255, 255, 255});
|
|
|
|
// --- Gravity HUD: show current gravity in ms and equivalent fps (top-right) ---
|
|
{
|
|
char gms[64];
|
|
double gms_val = game.getGravityMs();
|
|
double gfps = gms_val > 0.0 ? (1000.0 / gms_val) : 0.0;
|
|
snprintf(gms, sizeof(gms), "GRAV: %.0f ms (%.2f fps)", gms_val, gfps);
|
|
pixelFont.draw(renderer, LOGICAL_W - 260, 10, gms, 0.9f, {200, 200, 220, 255});
|
|
}
|
|
|
|
// Hold piece (if implemented)
|
|
if (game.held().type < PIECE_COUNT) {
|
|
pixelFont.draw(renderer, statsX + 10, statsY + statsH - 80, "HOLD", 1.0f, {255, 220, 0, 255});
|
|
drawSmallPiece(renderer, blocksTex, game.held().type, statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f);
|
|
}
|
|
|
|
// Pause overlay: don't draw pause UI when the exit-confirm popup is showing
|
|
if (game.isPaused() && !showExitConfirmPopup) {
|
|
// Semi-transparent overlay
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180);
|
|
SDL_FRect pauseOverlay{0, 0, LOGICAL_W, LOGICAL_H};
|
|
SDL_RenderFillRect(renderer, &pauseOverlay);
|
|
|
|
// Pause text
|
|
pixelFont.draw(renderer, LOGICAL_W * 0.5f - 80, LOGICAL_H * 0.5f - 20, "PAUSED", 2.0f, {255, 255, 255, 255});
|
|
pixelFont.draw(renderer, LOGICAL_W * 0.5f - 120, LOGICAL_H * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255});
|
|
}
|
|
|
|
// Exit confirmation popup (modal)
|
|
if (showExitConfirmPopup) {
|
|
// Compute content offsets for consistent placement across window sizes
|
|
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 popupW = 420, popupH = 180;
|
|
float popupX = (LOGICAL_W - popupW) / 2;
|
|
float popupY = (LOGICAL_H - popupH) / 2;
|
|
|
|
// Dim entire window (use window coordinates so it always covers 100% of the target)
|
|
SDL_SetRenderViewport(renderer, nullptr);
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200);
|
|
SDL_FRect fullWin{0.f, 0.f, (float)winW, (float)winH};
|
|
SDL_RenderFillRect(renderer, &fullWin);
|
|
// Restore logical viewport for drawing content-local popup
|
|
SDL_SetRenderViewport(renderer, &logicalVP);
|
|
|
|
// Draw popup box (drawRect will apply contentOffset internally)
|
|
drawRect(popupX - 4, popupY - 4, popupW + 8, popupH + 8, {60, 70, 90, 255});
|
|
drawRect(popupX, popupY, popupW, popupH, {20, 22, 28, 240});
|
|
|
|
// Center title and body text inside popup (use pixelFont for retro P2 font)
|
|
const std::string title = "Exit game?";
|
|
const std::string line1 = "Are you sure you want to";
|
|
const std::string line2 = "leave the current game?";
|
|
|
|
int wTitle=0,hTitle=0; pixelFont.measure( title, 1.6f, wTitle, hTitle);
|
|
int wL1=0,hL1=0; pixelFont.measure( line1, 0.9f, wL1, hL1);
|
|
int wL2=0,hL2=0; pixelFont.measure( line2, 0.9f, wL2, hL2);
|
|
|
|
float titleX = popupX + (popupW - (float)wTitle) * 0.5f;
|
|
float l1X = popupX + (popupW - (float)wL1) * 0.5f;
|
|
float l2X = popupX + (popupW - (float)wL2) * 0.5f;
|
|
|
|
pixelFont.draw(renderer, titleX + contentOffsetX, popupY + contentOffsetY + 20, title, 1.6f, {255, 220, 0, 255});
|
|
pixelFont.draw(renderer, l1X + contentOffsetX, popupY + contentOffsetY + 60, line1, 0.9f, SDL_Color{220,220,230,255});
|
|
pixelFont.draw(renderer, l2X + contentOffsetX, popupY + contentOffsetY + 84, line2, 0.9f, SDL_Color{220,220,230,255});
|
|
|
|
// Buttons (center labels inside buttons) - use pixelFont for labels
|
|
float btnW = 140, btnH = 46;
|
|
float yesX = popupX + popupW * 0.25f - btnW/2.0f;
|
|
float noX = popupX + popupW * 0.75f - btnW/2.0f;
|
|
float btnY = popupY + popupH - 60;
|
|
|
|
drawRect(yesX - 2, btnY - 2, btnW + 4, btnH + 4, {100, 120, 140, 255});
|
|
drawRect(yesX, btnY, btnW, btnH, {200, 60, 60, 255});
|
|
const std::string yes = "YES";
|
|
int wy=0,hy=0; pixelFont.measure( yes, 1.0f, wy, hy);
|
|
pixelFont.draw(renderer, yesX + (btnW - (float)wy) * 0.5f + contentOffsetX, btnY + (btnH - (float)hy) * 0.5f + contentOffsetY, yes, 1.0f, {255,255,255,255});
|
|
|
|
drawRect(noX - 2, btnY - 2, btnW + 4, btnH + 4, {100, 120, 140, 255});
|
|
drawRect(noX, btnY, btnW, btnH, {80, 140, 80, 255});
|
|
const std::string no = "NO";
|
|
int wn=0,hn=0; pixelFont.measure( no, 1.0f, wn, hn);
|
|
pixelFont.draw(renderer, noX + (btnW - (float)wn) * 0.5f + contentOffsetX, btnY + (btnH - (float)hn) * 0.5f + contentOffsetY, no, 1.0f, {255,255,255,255});
|
|
}
|
|
|
|
// Controls hint at bottom
|
|
font.draw(renderer, 20, LOGICAL_H - 30, "ARROWS=Move Z/X=Rotate C=Hold SPACE=Drop P=Pause ESC=Menu", 1.0f, {150, 150, 170, 255});
|
|
}
|
|
break;
|
|
case AppState::GameOver:
|
|
font.draw(renderer, LOGICAL_W * 0.5f - 120, 140, "GAME OVER", 3.0f, SDL_Color{255, 80, 60, 255});
|
|
{
|
|
char buf[128];
|
|
std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d", game.score(), game.lines(), game.level());
|
|
font.draw(renderer, LOGICAL_W * 0.5f - 120, 220, buf, 1.2f, SDL_Color{220, 220, 230, 255});
|
|
}
|
|
font.draw(renderer, LOGICAL_W * 0.5f - 120, 270, "PRESS ENTER / SPACE", 1.2f, SDL_Color{200, 200, 220, 255});
|
|
break;
|
|
}
|
|
|
|
SDL_RenderPresent(renderer);
|
|
SDL_SetRenderScale(renderer, 1.f, 1.f);
|
|
}
|
|
if (logoTex)
|
|
SDL_DestroyTexture(logoTex);
|
|
if (backgroundTex)
|
|
SDL_DestroyTexture(backgroundTex);
|
|
if (levelBackgroundTex)
|
|
SDL_DestroyTexture(levelBackgroundTex);
|
|
if (blocksTex)
|
|
SDL_DestroyTexture(blocksTex);
|
|
lineEffect.shutdown();
|
|
Audio::instance().shutdown();
|
|
SoundEffectManager::instance().shutdown();
|
|
font.shutdown();
|
|
TTF_Quit();
|
|
SDL_DestroyRenderer(renderer);
|
|
SDL_DestroyWindow(window);
|
|
SDL_Quit();
|
|
return 0;
|
|
}
|