Updated game speed
This commit is contained in:
96
src/Game.cpp
96
src/Game.cpp
@ -14,12 +14,45 @@ static const std::array<Shape, PIECE_COUNT> SHAPES = {{
|
||||
Shape{ 0x02E0, 0x4460, 0x0E80, 0xC440 }, // L
|
||||
}};
|
||||
|
||||
// NES (NTSC) gravity table: frames per grid cell for each level.
|
||||
// Based on: 0-9, 10-12: 5, 13-15: 4, 16-18: 3, 19-28: 2, 29+: 1
|
||||
namespace {
|
||||
constexpr double NES_FPS = 60.0988;
|
||||
constexpr double FRAME_MS = 1000.0 / NES_FPS;
|
||||
|
||||
struct LevelGravity { int framesPerCell; double levelMultiplier; };
|
||||
|
||||
// Default table following NES values; levelMultiplier starts at 1.0 and can be tuned per-level
|
||||
LevelGravity LEVEL_TABLE[30] = {
|
||||
{48,1.0}, {43,1.0}, {38,1.0}, {33,1.0}, {28,1.0}, {23,1.0}, {18,1.0}, {13,1.0}, {8,1.0}, {6,1.0},
|
||||
{5,1.0}, {5,1.0}, {5,1.0}, {4,1.0}, {4,1.0}, {4,1.0}, {3,1.0}, {3,1.0}, {3,1.0}, {2,1.0},
|
||||
{2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {1,1.0}
|
||||
};
|
||||
|
||||
inline double gravityMsForLevel(int level, double globalMultiplier)
|
||||
{
|
||||
int idx = level < 0 ? 0 : (level >= 29 ? 29 : level);
|
||||
const LevelGravity &lg = LEVEL_TABLE[idx];
|
||||
double frames = lg.framesPerCell * lg.levelMultiplier;
|
||||
double result = frames * FRAME_MS * globalMultiplier;
|
||||
|
||||
static int debug_calls = 0;
|
||||
if (debug_calls < 3) {
|
||||
printf("Level %d: %d frames per cell (mult %.2f) = %.1f ms per cell (global x%.2f)\\n",
|
||||
level, lg.framesPerCell, lg.levelMultiplier, result, globalMultiplier);
|
||||
debug_calls++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
void Game::reset(int startLevel_) {
|
||||
std::fill(board.begin(), board.end(), 0);
|
||||
std::fill(blockCounts.begin(), blockCounts.end(), 0);
|
||||
bag.clear();
|
||||
_score = 0; _lines = 0; _level = startLevel_ + 1; startLevel = startLevel_;
|
||||
gravityMs = 800.0 * std::pow(0.85, startLevel_); // speed-up for higher starts
|
||||
_score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_;
|
||||
// Initialize gravity using NES timing table (ms per cell by level)
|
||||
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
|
||||
fallAcc = 0; _elapsedSec = 0; gameOver=false; paused=false;
|
||||
hold = Piece{}; hold.type = PIECE_COUNT; canHold=true;
|
||||
refillBag(); spawn();
|
||||
@ -31,6 +64,18 @@ void Game::refillBag() {
|
||||
std::shuffle(bag.begin(), bag.end(), rng);
|
||||
}
|
||||
|
||||
double Game::getGravityGlobalMultiplier() const { return gravityGlobalMultiplier; }
|
||||
double Game::getGravityMs() const { return gravityMs; }
|
||||
|
||||
void Game::setLevelGravityMultiplier(int level, double m) {
|
||||
if (level < 0) return;
|
||||
int idx = level >= 29 ? 29 : level;
|
||||
LEVEL_TABLE[idx].levelMultiplier = m;
|
||||
// If current level changed, refresh gravityMs
|
||||
if (_level == idx) gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
|
||||
}
|
||||
|
||||
|
||||
void Game::spawn() {
|
||||
if (bag.empty()) refillBag();
|
||||
PieceType pieceType = bag.back();
|
||||
@ -97,14 +142,15 @@ void Game::lockPiece() {
|
||||
case 4: base = 1200; break; // TETRIS
|
||||
default: base = 0; break;
|
||||
}
|
||||
_score += base * std::max(1, _level);
|
||||
// multiplier is level+1 to match original scoring where level 0 => x1
|
||||
_score += base * (_level + 1);
|
||||
|
||||
// Update total lines
|
||||
_lines += cleared;
|
||||
|
||||
// JS level progression (NES-like) using starting level rules
|
||||
// startLevel is 0-based in JS; our _level is JS level + 1
|
||||
const int threshold = (startLevel + 1) * 10;
|
||||
// Both startLevel and _level are 0-based now.
|
||||
const int threshold = (startLevel + 1) * 10; // first promotion after this many lines
|
||||
int oldLevel = _level;
|
||||
// First level up happens when total lines equal threshold
|
||||
// After that, every 10 lines (when (lines - threshold) % 10 == 0)
|
||||
@ -115,10 +161,9 @@ void Game::lockPiece() {
|
||||
}
|
||||
|
||||
if (_level > oldLevel) {
|
||||
gravityMs = std::max(60.0, gravityMs * 0.85);
|
||||
if (levelUpCallback) {
|
||||
levelUpCallback(_level);
|
||||
}
|
||||
// Update gravity to exact NES speed for the new level
|
||||
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
|
||||
if (levelUpCallback) levelUpCallback(_level);
|
||||
}
|
||||
|
||||
// Trigger sound effect callback for line clears
|
||||
@ -210,7 +255,8 @@ void Game::tickGravity(double frameMs) {
|
||||
}
|
||||
|
||||
void Game::softDropBoost(double frameMs) {
|
||||
if (!paused) fallAcc += frameMs * 10.0;
|
||||
// Reduce soft drop speed multiplier from 10.0 to 3.0 to make it less aggressive
|
||||
if (!paused) fallAcc += frameMs * 3.0;
|
||||
}
|
||||
|
||||
void Game::hardDrop() {
|
||||
@ -227,8 +273,34 @@ void Game::hardDrop() {
|
||||
|
||||
void Game::rotate(int dir) {
|
||||
if (paused) return;
|
||||
Piece p = cur; p.rot = (p.rot + dir + 4) % 4; const int kicks[5]={0,-1,1,-2,2};
|
||||
for (int dx : kicks) { p.x = cur.x + dx; if (!collides(p)) { cur = p; return; } }
|
||||
|
||||
Piece p = cur;
|
||||
p.rot = (p.rot + dir + 4) % 4;
|
||||
|
||||
// Try rotation at current position first
|
||||
if (!collides(p)) {
|
||||
cur = p;
|
||||
return;
|
||||
}
|
||||
|
||||
// JavaScript-style wall kicks: try horizontal, up, then larger horizontal offsets
|
||||
const std::pair<int,int> kicks[] = {
|
||||
{1, 0}, // right
|
||||
{-1, 0}, // left
|
||||
{0, -1}, // up (key difference from our previous approach)
|
||||
{2, 0}, // 2 right (for I piece)
|
||||
{-2, 0}, // 2 left (for I piece)
|
||||
};
|
||||
|
||||
for (auto kick : kicks) {
|
||||
Piece test = p;
|
||||
test.x = cur.x + kick.first;
|
||||
test.y = cur.y + kick.second;
|
||||
if (!collides(test)) {
|
||||
cur = test;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Game::move(int dx) {
|
||||
|
||||
10
src/Game.h
10
src/Game.h
@ -62,6 +62,12 @@ public:
|
||||
// Shape helper --------------------------------------------------------
|
||||
static bool cellFilled(const Piece& p, int cx, int cy);
|
||||
|
||||
// Gravity tuning accessors (public API for HUD/tuning)
|
||||
void setGravityGlobalMultiplier(double m) { gravityGlobalMultiplier = m; }
|
||||
double getGravityGlobalMultiplier() const;
|
||||
double getGravityMs() const;
|
||||
void setLevelGravityMultiplier(int level, double m);
|
||||
|
||||
private:
|
||||
std::array<int, COLS*ROWS> board{}; // 0 empty else color index
|
||||
Piece cur{}, hold{}, nextPiece{}; // current, held & next piece
|
||||
@ -87,6 +93,9 @@ private:
|
||||
// Sound effect callbacks
|
||||
SoundCallback soundCallback;
|
||||
LevelUpCallback levelUpCallback;
|
||||
// Gravity tuning -----------------------------------------------------
|
||||
// Global multiplier applied to all level timings (use to slow/speed whole-game gravity)
|
||||
double gravityGlobalMultiplier{2.8};
|
||||
|
||||
// Internal helpers ----------------------------------------------------
|
||||
void refillBag();
|
||||
@ -96,4 +105,5 @@ private:
|
||||
int checkLines(); // Find completed lines and store them
|
||||
void actualClearLines(); // Actually remove lines from board
|
||||
bool tryMoveDown(); // one-row fall; returns true if moved
|
||||
// Gravity tuning helpers (public API declared above)
|
||||
};
|
||||
|
||||
@ -284,10 +284,10 @@ void LineEffect::playLineClearSound(int lineCount) {
|
||||
|
||||
if (lineCount == 4) {
|
||||
sample = &tetrisSample; // Special sound for Tetris
|
||||
printf("TETRIS! 4 lines cleared!\n");
|
||||
//printf("TETRIS! 4 lines cleared!\n");
|
||||
} else {
|
||||
sample = &lineClearSample; // Regular line clear sound
|
||||
printf("Line clear: %d lines\n", lineCount);
|
||||
//printf("Line clear: %d lines\n", lineCount);
|
||||
}
|
||||
|
||||
if (sample && !sample->empty()) {
|
||||
|
||||
@ -43,8 +43,7 @@ bool SoundEffect::load(const std::string& filePath) {
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
std::printf("[SoundEffect] Loaded: %s (%d channels, %d Hz, %zu samples)\n",
|
||||
filePath.c_str(), channels, sampleRate, pcmData.size());
|
||||
//std::printf("[SoundEffect] Loaded: %s (%d channels, %d Hz, %zu samples)\n", filePath.c_str(), channels, sampleRate, pcmData.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -54,7 +53,7 @@ void SoundEffect::play(float volume) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::printf("[SoundEffect] Playing sound with %zu samples at volume %.2f\n", pcmData.size(), volume);
|
||||
//std::printf("[SoundEffect] Playing sound with %zu samples at volume %.2f\n", pcmData.size(), volume);
|
||||
|
||||
// Calculate final volume
|
||||
float finalVolume = defaultVolume * volume;
|
||||
@ -80,13 +79,13 @@ bool SimpleAudioPlayer::init() {
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
std::printf("[SimpleAudioPlayer] Initialized\n");
|
||||
//std::printf("[SimpleAudioPlayer] Initialized\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
void SimpleAudioPlayer::shutdown() {
|
||||
initialized = false;
|
||||
std::printf("[SimpleAudioPlayer] Shut down\n");
|
||||
//std::printf("[SimpleAudioPlayer] Shut down\n");
|
||||
}
|
||||
|
||||
void SimpleAudioPlayer::playSound(const std::vector<int16_t>& pcmData, int channels, int sampleRate, float volume) {
|
||||
@ -244,7 +243,7 @@ bool SoundEffectManager::init() {
|
||||
SimpleAudioPlayer::instance().init();
|
||||
|
||||
initialized = true;
|
||||
std::printf("[SoundEffectManager] Initialized\n");
|
||||
//std::printf("[SoundEffectManager] Initialized\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -252,7 +251,7 @@ void SoundEffectManager::shutdown() {
|
||||
soundEffects.clear();
|
||||
SimpleAudioPlayer::instance().shutdown();
|
||||
initialized = false;
|
||||
std::printf("[SoundEffectManager] Shut down\n");
|
||||
//std::printf("[SoundEffectManager] Shut down\n");
|
||||
}
|
||||
|
||||
bool SoundEffectManager::loadSound(const std::string& id, const std::string& filePath) {
|
||||
@ -274,7 +273,7 @@ bool SoundEffectManager::loadSound(const std::string& id, const std::string& fil
|
||||
soundEffects.end());
|
||||
|
||||
soundEffects.emplace_back(id, std::move(soundEffect));
|
||||
std::printf("[SoundEffectManager] Loaded sound '%s' from %s\n", id.c_str(), filePath.c_str());
|
||||
//std::printf("[SoundEffectManager] Loaded sound '%s' from %s\n", id.c_str(), filePath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
5
src/app_icon.rc
Normal file
5
src/app_icon.rc
Normal file
@ -0,0 +1,5 @@
|
||||
/* Windows resource script to embed the application icon */
|
||||
/* Uses the project's favicon icon located in assets/favicon/favicon.ico */
|
||||
|
||||
// Simple icon resource - the icon will be copied to the build directory by CMake
|
||||
1 ICON "favicon.ico"
|
||||
640
src/main.cpp
640
src/main.cpp
@ -26,6 +26,9 @@
|
||||
#include "states/State.h"
|
||||
#include "states/LoadingState.h"
|
||||
#include "states/MenuState.h"
|
||||
#include "states/PlayingState.h"
|
||||
|
||||
// Debug logging removed: no-op in this build (previously LOG_DEBUG)
|
||||
|
||||
// Font rendering now handled by FontAtlas
|
||||
|
||||
@ -89,6 +92,43 @@ static void drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx
|
||||
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
|
||||
// -----------------------------------------------------------------------------
|
||||
@ -98,7 +138,7 @@ static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, flo
|
||||
if (!blocksTex) {
|
||||
static bool printed = false;
|
||||
if (!printed) {
|
||||
printf("drawBlockTexture: No texture available, using colored rectangles\n");
|
||||
(void)0;
|
||||
printed = true;
|
||||
}
|
||||
}
|
||||
@ -184,47 +224,69 @@ static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, Piece
|
||||
// -----------------------------------------------------------------------------
|
||||
// Popup Drawing Functions
|
||||
// -----------------------------------------------------------------------------
|
||||
static void drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, int selectedLevel) {
|
||||
float popupW = 400, popupH = 300;
|
||||
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, "SELECT STARTING LEVEL", 2.0f, {255, 220, 0, 255});
|
||||
|
||||
// Level grid (4x5 = 20 levels, 0-19)
|
||||
float gridStartX = popupX + 50;
|
||||
float gridStartY = popupY + 70;
|
||||
float cellW = 70, cellH = 35;
|
||||
|
||||
for (int level = 0; level < 20; level++) {
|
||||
int row = level / 4;
|
||||
int col = level % 4;
|
||||
float cellX = gridStartX + col * cellW;
|
||||
float cellY = gridStartY + row * cellH;
|
||||
|
||||
bool isSelected = (level == selectedLevel);
|
||||
SDL_Color cellColor = isSelected ? SDL_Color{255, 220, 0, 255} : SDL_Color{80, 100, 140, 255};
|
||||
SDL_Color textColor = isSelected ? SDL_Color{0, 0, 0, 255} : SDL_Color{255, 255, 255, 255};
|
||||
|
||||
drawRect(renderer, cellX, cellY, cellW-5, cellH-5, cellColor);
|
||||
|
||||
char levelStr[8];
|
||||
snprintf(levelStr, sizeof(levelStr), "%d", level);
|
||||
font.draw(renderer, cellX + 25, cellY + 8, levelStr, 1.2f, textColor);
|
||||
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);
|
||||
}
|
||||
|
||||
// Instructions
|
||||
font.draw(renderer, popupX + 20, popupY + 230, "CLICK TO SELECT • ESC TO CANCEL", 1.0f, {200, 200, 220, 255});
|
||||
|
||||
// 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) {
|
||||
@ -279,92 +341,90 @@ static bool showLevelPopup = false;
|
||||
static bool showSettingsPopup = false;
|
||||
static bool musicEnabled = true;
|
||||
static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings
|
||||
// Shared texture for fireworks particles (uses the blocks sheet)
|
||||
static SDL_Texture* fireworksBlocksTex = nullptr;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tetris Block Fireworks for intro animation
|
||||
// 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 TetrisParticle {
|
||||
struct BlockParticle {
|
||||
float x, y, vx, vy, size, alpha, decay;
|
||||
SDL_Color color;
|
||||
bool hasTrail;
|
||||
std::vector<std::pair<float, float>> trail;
|
||||
|
||||
TetrisParticle(float x_, float y_, SDL_Color color_)
|
||||
: x(x_), y(y_), color(color_), hasTrail(false) {
|
||||
float angle = (rand() % 628) / 100.0f; // 0 to 2π
|
||||
float speed = 1 + (rand() % 400) / 100.0f; // 1 to 5
|
||||
vx = cos(angle) * speed;
|
||||
vy = sin(angle) * speed;
|
||||
size = 1 + (rand() % 200) / 100.0f; // 1 to 3
|
||||
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.01f + (rand() % 200) / 10000.0f; // 0.01 to 0.03
|
||||
hasTrail = (rand() % 100) < 30; // 30% chance
|
||||
decay = 0.012f + (rand() % 200) / 10000.0f; // 0.012..0.032
|
||||
blockType = rand() % 7; // choose a tetris color
|
||||
}
|
||||
|
||||
bool update() {
|
||||
if (hasTrail) {
|
||||
trail.push_back({x, y});
|
||||
if (trail.size() > 5) trail.erase(trail.begin());
|
||||
}
|
||||
|
||||
vx *= 0.98f; // friction
|
||||
vy = vy * 0.98f + 0.06f; // gravity
|
||||
vx *= 0.985f; // friction
|
||||
vy = vy * 0.985f + 0.07f; // gravity
|
||||
x += vx;
|
||||
y += vy;
|
||||
alpha -= decay;
|
||||
if (size > 0.1f) size -= 0.03f;
|
||||
|
||||
return alpha > 0;
|
||||
size = std::max(2.0f, size - 0.04f);
|
||||
return alpha > 0.02f;
|
||||
}
|
||||
};
|
||||
|
||||
struct TetrisFirework {
|
||||
std::vector<TetrisParticle> particles;
|
||||
|
||||
std::vector<BlockParticle> particles;
|
||||
int mode = 0; // 0=random,1=red,2=green,3=palette
|
||||
TetrisFirework(float x, float y) {
|
||||
SDL_Color colors[] = {
|
||||
{255, 255, 0, 255}, // Yellow
|
||||
{0, 255, 255, 255}, // Cyan
|
||||
{255, 0, 255, 255}, // Magenta
|
||||
{0, 255, 0, 255}, // Green
|
||||
{255, 0, 0, 255}, // Red
|
||||
{0, 0, 255, 255}, // Blue
|
||||
{255, 160, 0, 255} // Orange
|
||||
};
|
||||
|
||||
int particleCount = 20 + rand() % 21; // 20-40 particles
|
||||
for (int i = 0; i < particleCount; i++) {
|
||||
SDL_Color color = colors[rand() % 7];
|
||||
particles.emplace_back(x, y, color);
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (!it->update()) it = particles.erase(it); else ++it;
|
||||
}
|
||||
return !particles.empty();
|
||||
}
|
||||
|
||||
void draw(SDL_Renderer* renderer) {
|
||||
for (auto& p : particles) {
|
||||
// Draw trail
|
||||
if (p.hasTrail && p.trail.size() > 1) {
|
||||
for (size_t i = 1; i < p.trail.size(); i++) {
|
||||
float trailAlpha = p.alpha * 0.3f * (float(i) / p.trail.size());
|
||||
SDL_SetRenderDrawColor(renderer, p.color.r, p.color.g, p.color.b, Uint8(trailAlpha * 255));
|
||||
SDL_RenderLine(renderer, p.trail[i-1].first, p.trail[i-1].second, p.trail[i].first, p.trail[i].second);
|
||||
for (auto &p : particles) {
|
||||
if (fireworksBlocksTex) {
|
||||
// Apply per-particle alpha and color variants by modulating the blocks texture
|
||||
// Save previous mods (assume single-threaded rendering)
|
||||
Uint8 prevA = 255;
|
||||
SDL_GetTextureAlphaMod(fireworksBlocksTex, &prevA);
|
||||
Uint8 setA = Uint8(std::max(0.0f, std::min(1.0f, p.alpha)) * 255.0f);
|
||||
SDL_SetTextureAlphaMod(fireworksBlocksTex, setA);
|
||||
|
||||
// Color modes: tint the texture where appropriate
|
||||
if (mode == 1) {
|
||||
// red
|
||||
SDL_SetTextureColorMod(fireworksBlocksTex, 220, 60, 60);
|
||||
} else if (mode == 2) {
|
||||
// green
|
||||
SDL_SetTextureColorMod(fireworksBlocksTex, 80, 200, 80);
|
||||
} else if (mode == 3) {
|
||||
// tint to the particle's block color
|
||||
SDL_Color c = COLORS[p.blockType + 1];
|
||||
SDL_SetTextureColorMod(fireworksBlocksTex, c.r, c.g, c.b);
|
||||
} else {
|
||||
// random: no tint (use texture colors directly)
|
||||
SDL_SetTextureColorMod(fireworksBlocksTex, 255, 255, 255);
|
||||
}
|
||||
|
||||
drawBlockTexture(renderer, fireworksBlocksTex, p.x - p.size * 0.5f, p.y - p.size * 0.5f, p.size, p.blockType);
|
||||
|
||||
// Restore alpha and color modulation
|
||||
SDL_SetTextureAlphaMod(fireworksBlocksTex, prevA);
|
||||
SDL_SetTextureColorMod(fireworksBlocksTex, 255, 255, 255);
|
||||
} else {
|
||||
SDL_SetRenderDrawColor(renderer, 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(renderer, &rect);
|
||||
}
|
||||
|
||||
// Draw particle
|
||||
SDL_SetRenderDrawColor(renderer, p.color.r, p.color.g, p.color.b, Uint8(p.alpha * 255));
|
||||
SDL_FRect rect{p.x - p.size/2, p.y - p.size/2, p.size, p.size};
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -377,11 +437,10 @@ static Uint64 lastFireworkTime = 0;
|
||||
// -----------------------------------------------------------------------------
|
||||
static void updateFireworks(double frameMs) {
|
||||
Uint64 now = SDL_GetTicks();
|
||||
|
||||
// Randomly spawn new fireworks (2% chance per frame)
|
||||
if (fireworks.size() < 6 && (rand() % 100) < 2) {
|
||||
float x = 100 + rand() % (LOGICAL_W - 200);
|
||||
float y = 100 + rand() % (LOGICAL_H - 300);
|
||||
// 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;
|
||||
}
|
||||
@ -397,22 +456,29 @@ static void updateFireworks(double frameMs) {
|
||||
}
|
||||
|
||||
static void drawFireworks(SDL_Renderer* renderer) {
|
||||
for (auto& firework : fireworks) {
|
||||
firework.draw(renderer);
|
||||
}
|
||||
for (auto& f : fireworks) f.draw(renderer);
|
||||
}
|
||||
|
||||
// External wrappers for use by other translation units (MenuState)
|
||||
// These call the internal helpers above so we don't change existing static linkage.
|
||||
void menu_drawFireworks(SDL_Renderer* renderer) { drawFireworks(renderer); }
|
||||
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()));
|
||||
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0)
|
||||
int sdlInitRes = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
|
||||
if (sdlInitRes < 0)
|
||||
{
|
||||
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
if (TTF_Init() < 0)
|
||||
int ttfInitRes = TTF_Init();
|
||||
if (ttfInitRes < 0)
|
||||
{
|
||||
std::fprintf(stderr, "TTF_Init failed\n");
|
||||
SDL_Quit();
|
||||
@ -457,43 +523,61 @@ int main(int, char **)
|
||||
|
||||
// Load logo using native SDL BMP loading
|
||||
SDL_Texture *logoTex = nullptr;
|
||||
SDL_Surface* logoSurface = SDL_LoadBMP("assets/images/logo_small.bmp");
|
||||
SDL_Surface* logoSurface = SDL_LoadBMP("assets/images/logo.bmp");
|
||||
if (logoSurface) {
|
||||
printf("Successfully loaded logo_small.bmp using native SDL\n");
|
||||
(void)0;
|
||||
logoTex = SDL_CreateTextureFromSurface(renderer, logoSurface);
|
||||
SDL_DestroySurface(logoSurface);
|
||||
} else {
|
||||
printf("Warning: logo_small.bmp not found\n");
|
||||
(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) {
|
||||
printf("Successfully loaded main_background.bmp using native SDL\n");
|
||||
(void)0;
|
||||
backgroundTex = SDL_CreateTextureFromSurface(renderer, backgroundSurface);
|
||||
SDL_DestroySurface(backgroundSurface);
|
||||
} else {
|
||||
printf("Warning: main_background.bmp not found\n");
|
||||
(void)0;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
printf("Successfully loaded blocks90px_001.bmp using native SDL\n");
|
||||
(void)0;
|
||||
blocksTex = SDL_CreateTextureFromSurface(renderer, blocksSurface);
|
||||
SDL_DestroySurface(blocksSurface);
|
||||
} else {
|
||||
printf("Warning: blocks90px_001.bmp not found, creating programmatic texture...\n");
|
||||
(void)0;
|
||||
}
|
||||
|
||||
if (!blocksTex) {
|
||||
printf("All image formats failed, creating blocks texture programmatically...\n");
|
||||
(void)0;
|
||||
|
||||
// Create a 630x90 texture (7 blocks * 90px each)
|
||||
blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90);
|
||||
@ -521,14 +605,18 @@ int main(int, char **)
|
||||
|
||||
// Reset render target
|
||||
SDL_SetRenderTarget(renderer, nullptr);
|
||||
printf("Successfully created programmatic blocks texture\n");
|
||||
(void)0;
|
||||
} else {
|
||||
printf("Failed to create programmatic texture: %s\n", SDL_GetError());
|
||||
std::fprintf(stderr, "Failed to create programmatic texture: %s\n", SDL_GetError());
|
||||
}
|
||||
} else {
|
||||
printf("Successfully loaded PNG blocks texture\n");
|
||||
(void)0;
|
||||
}
|
||||
|
||||
// Provide the blocks sheet to the fireworks system (for block particles)
|
||||
fireworksBlocksTex = blocksTex;
|
||||
|
||||
// Default start level selection: 0
|
||||
int startLevelSelection = 0;
|
||||
Game game(startLevelSelection);
|
||||
|
||||
@ -553,7 +641,7 @@ int main(int, char **)
|
||||
if (wavFile) {
|
||||
SDL_CloseIO(wavFile);
|
||||
if (SoundEffectManager::instance().loadSound(id, wavPath)) {
|
||||
printf("Loaded WAV: %s\n", wavPath.c_str());
|
||||
(void)0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -563,12 +651,12 @@ int main(int, char **)
|
||||
if (mp3File) {
|
||||
SDL_CloseIO(mp3File);
|
||||
if (SoundEffectManager::instance().loadSound(id, mp3Path)) {
|
||||
printf("Loaded MP3: %s\n", mp3Path.c_str());
|
||||
(void)0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
printf("Failed to load sound: %s (tried both WAV and MP3)\n", id.c_str());
|
||||
std::fprintf(stderr, "Failed to load sound: %s (tried both WAV and MP3)\n", id.c_str());
|
||||
};
|
||||
|
||||
loadSoundWithFallback("nice_combo", "nice_combo");
|
||||
@ -634,15 +722,21 @@ int main(int, char **)
|
||||
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;
|
||||
|
||||
// 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); });
|
||||
@ -653,29 +747,31 @@ int main(int, char **)
|
||||
stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); });
|
||||
stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); });
|
||||
|
||||
// Minimal Playing state handler (gameplay input) - kept inline until PlayingState is implemented
|
||||
// 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()) {
|
||||
// Hard drop (Space)
|
||||
if (e.key.scancode == SDL_SCANCODE_SPACE) {
|
||||
game.hardDrop();
|
||||
}
|
||||
// Rotate clockwise (Up arrow)
|
||||
else if (e.key.scancode == SDL_SCANCODE_UP) {
|
||||
game.rotate(+1);
|
||||
}
|
||||
// Rotate counter-clockwise (Z) or Shift modifier
|
||||
else if (e.key.scancode == SDL_SCANCODE_Z || (e.key.mod & SDL_KMOD_SHIFT)) {
|
||||
game.rotate(-1);
|
||||
}
|
||||
// Hold current piece (C) or Ctrl modifier
|
||||
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)
|
||||
@ -736,6 +832,12 @@ int main(int, char **)
|
||||
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;
|
||||
@ -767,15 +869,23 @@ int main(int, char **)
|
||||
// Click anywhere closes settings popup
|
||||
showSettingsPopup = false;
|
||||
} else {
|
||||
// Main menu buttons
|
||||
SDL_FRect playBtn{LOGICAL_W * 0.5f - 100, LOGICAL_H * 0.75f, 200, 50};
|
||||
SDL_FRect levelBtn{LOGICAL_W * 0.5f - 100, LOGICAL_H * 0.75f + 60, 200, 50};
|
||||
|
||||
// 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);
|
||||
game.reset(startLevelSelection);
|
||||
}
|
||||
else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h)
|
||||
{
|
||||
@ -806,9 +916,19 @@ int main(int, char **)
|
||||
float lx = (mx - logicalVP.x) / logicalScale, ly = (my - logicalVP.y) / logicalScale;
|
||||
if (state == AppState::Menu && !showLevelPopup && !showSettingsPopup)
|
||||
{
|
||||
// Check button hover states
|
||||
SDL_FRect playBtn{LOGICAL_W * 0.5f - 100, LOGICAL_H * 0.75f, 200, 50};
|
||||
SDL_FRect levelBtn{LOGICAL_W * 0.5f - 100, LOGICAL_H * 0.75f + 60, 200, 50};
|
||||
// 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)
|
||||
@ -973,6 +1093,12 @@ int main(int, char **)
|
||||
} 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) {
|
||||
@ -988,6 +1114,9 @@ int main(int, char **)
|
||||
case AppState::Menu:
|
||||
menuState->update(frameMs);
|
||||
break;
|
||||
case AppState::Playing:
|
||||
playingState->update(frameMs);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -1005,41 +1134,64 @@ int main(int, char **)
|
||||
|
||||
// Only load new background if level changed
|
||||
if (cachedLevel != bgLevel) {
|
||||
// Clean up old texture
|
||||
if (levelBackgroundTex) {
|
||||
SDL_DestroyTexture(levelBackgroundTex);
|
||||
levelBackgroundTex = nullptr;
|
||||
}
|
||||
|
||||
// Load new level background
|
||||
// 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) {
|
||||
levelBackgroundTex = SDL_CreateTextureFromSurface(renderer, levelBgSurface);
|
||||
nextLevelBackgroundTex = SDL_CreateTextureFromSurface(renderer, levelBgSurface);
|
||||
SDL_DestroySurface(levelBgSurface);
|
||||
// start fade transition
|
||||
levelFadeAlpha = 0.0f;
|
||||
levelFadeElapsed = 0.0f;
|
||||
cachedLevel = bgLevel;
|
||||
printf("Loaded level background for level %d\n", bgLevel);
|
||||
} else {
|
||||
printf("Warning: Could not load level background for level %d\n", bgLevel);
|
||||
cachedLevel = -1; // Mark as failed
|
||||
// don't change textures if file missing
|
||||
cachedLevel = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw cached level background if available
|
||||
if (levelBackgroundTex) {
|
||||
// Stretch background to full viewport
|
||||
|
||||
// Draw blended backgrounds if needed
|
||||
if (levelBackgroundTex || nextLevelBackgroundTex) {
|
||||
SDL_FRect fullRect = { 0, 0, (float)logicalVP.w, (float)logicalVP.h };
|
||||
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
|
||||
// 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_SetRenderViewport(renderer, &logicalVP);
|
||||
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
|
||||
|
||||
switch (state)
|
||||
@ -1059,11 +1211,11 @@ int main(int, char **)
|
||||
// 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 = 24; // Height of "LOADING" text
|
||||
const float barHeight = 24; // Loading bar height
|
||||
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 ? 8 : 20;
|
||||
const float spacingBetweenElements = isLimitedHeight ? 5 : 15;
|
||||
|
||||
// Total content height
|
||||
const float totalContentHeight = logoHeight +
|
||||
@ -1080,18 +1232,18 @@ int main(int, char **)
|
||||
// Draw logo (centered, static like JavaScript version)
|
||||
if (logoTex)
|
||||
{
|
||||
// Original logo_small.bmp dimensions
|
||||
const int lw = 436, lh = 137;
|
||||
|
||||
// Calculate scaling like JavaScript version
|
||||
const float maxLogoWidth = LOGICAL_W * 0.9f;
|
||||
// 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 / lw;
|
||||
const float scaleFactorHeight = availableHeight / lh;
|
||||
|
||||
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;
|
||||
@ -1106,12 +1258,12 @@ int main(int, char **)
|
||||
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.5f, {255, 204, 0, 255});
|
||||
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 = 300, barH = 24;
|
||||
const int barW = 400, barH = 20;
|
||||
const int bx = (LOGICAL_W - barW) / 2;
|
||||
|
||||
// Bar border (dark gray) - using drawRect which adds content offset
|
||||
@ -1136,138 +1288,9 @@ int main(int, char **)
|
||||
}
|
||||
break;
|
||||
case AppState::Menu:
|
||||
{
|
||||
// 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 background if available
|
||||
if (backgroundTex) {
|
||||
SDL_FRect bgRect{contentOffsetX, contentOffsetY, LOGICAL_W, LOGICAL_H};
|
||||
SDL_RenderTexture(renderer, backgroundTex, nullptr, &bgRect);
|
||||
}
|
||||
|
||||
// Draw the enhanced intro screen with logo animation and fireworks
|
||||
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); };
|
||||
|
||||
// Draw animated logo with sine wave slicing effect (like JS version)
|
||||
if (logoTex)
|
||||
{
|
||||
// logo_small.bmp dimensions
|
||||
int lw = 436, lh = 137;
|
||||
int dw = int(LOGICAL_W * 0.5f); // Appropriate size for small logo
|
||||
int dh = dw * lh / lw;
|
||||
float logoX = (LOGICAL_W - dw) / 2.f + contentOffsetX;
|
||||
float logoY = LOGICAL_H * 0.08f + contentOffsetY; // Higher position like JS
|
||||
|
||||
// Animate logo with sine wave slices (port from JS version)
|
||||
for (int slice = 0; slice < dw; slice += 4) {
|
||||
float offsetY = sin(logoAnimCounter + slice * 0.01f) * 8.0f;
|
||||
SDL_FRect srcRect = {float(slice * lw / dw), 0, float(4 * lw / dw), float(lh)};
|
||||
SDL_FRect dstRect = {logoX + slice, logoY + offsetY, 4, float(dh)};
|
||||
SDL_RenderTexture(renderer, logoTex, &srcRect, &dstRect);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
font.draw(renderer, LOGICAL_W * 0.5f - 180 + contentOffsetX, LOGICAL_H * 0.1f + contentOffsetY, "TETRIS", 4.0f, SDL_Color{180, 200, 255, 255});
|
||||
}
|
||||
|
||||
// Cloud high scores badge (like JS version)
|
||||
float badgeWidth = 170;
|
||||
float badgeHeight = 30;
|
||||
float badgeX = LOGICAL_W - badgeWidth - 10 + contentOffsetX;
|
||||
float badgeY = 10 + contentOffsetY;
|
||||
|
||||
drawRect(badgeX - contentOffsetX, badgeY - contentOffsetY, badgeWidth, badgeHeight, {0, 0, 0, 178}); // Semi-transparent background
|
||||
font.draw(renderer, badgeX + 5, badgeY + 8, "CLOUD HIGH SCORES", 1.0f, {66, 133, 244, 255}); // Google blue
|
||||
|
||||
// "TOP PLAYERS" section (positioned like JS version)
|
||||
float topPlayersY = LOGICAL_H * 0.28f + contentOffsetY;
|
||||
font.draw(renderer, LOGICAL_W * 0.5f - 120 + contentOffsetX, topPlayersY, "TOP PLAYERS", 2.5f, SDL_Color{255, 220, 0, 255});
|
||||
|
||||
// High scores table with proper columns (like JS version)
|
||||
float scoresStartY = topPlayersY + 60;
|
||||
const auto &hs = scores.all();
|
||||
|
||||
// Column headers
|
||||
float headerY = scoresStartY - 30;
|
||||
font.draw(renderer, 40 + contentOffsetX, headerY, "RANK", 1.0f, SDL_Color{255, 204, 0, 255});
|
||||
font.draw(renderer, 120 + contentOffsetX, headerY, "PLAYER", 1.0f, SDL_Color{255, 204, 0, 255});
|
||||
font.draw(renderer, 280 + contentOffsetX, headerY, "SCORE", 1.0f, SDL_Color{255, 204, 0, 255});
|
||||
font.draw(renderer, 400 + contentOffsetX, headerY, "LINES", 1.0f, SDL_Color{255, 204, 0, 255});
|
||||
font.draw(renderer, 500 + contentOffsetX, headerY, "LEVEL", 1.0f, SDL_Color{255, 204, 0, 255});
|
||||
font.draw(renderer, 600 + contentOffsetX, headerY, "TIME", 1.0f, SDL_Color{255, 204, 0, 255});
|
||||
|
||||
// Display high scores (limit to 12 like JS version)
|
||||
size_t maxDisplay = std::min(hs.size(), size_t(12));
|
||||
for (size_t i = 0; i < maxDisplay; ++i)
|
||||
{
|
||||
float y = scoresStartY + i * 25;
|
||||
|
||||
// Rank
|
||||
char rankStr[8];
|
||||
snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1);
|
||||
font.draw(renderer, 40 + contentOffsetX, y, rankStr, 1.1f, SDL_Color{220, 220, 230, 255});
|
||||
|
||||
// Player name
|
||||
font.draw(renderer, 120 + contentOffsetX, y, hs[i].name, 1.1f, SDL_Color{220, 220, 230, 255});
|
||||
|
||||
// Score
|
||||
char scoreStr[16];
|
||||
snprintf(scoreStr, sizeof(scoreStr), "%d", hs[i].score);
|
||||
font.draw(renderer, 280 + contentOffsetX, y, scoreStr, 1.1f, SDL_Color{220, 220, 230, 255});
|
||||
|
||||
// Lines
|
||||
char linesStr[8];
|
||||
snprintf(linesStr, sizeof(linesStr), "%d", hs[i].lines);
|
||||
font.draw(renderer, 400 + contentOffsetX, y, linesStr, 1.1f, SDL_Color{220, 220, 230, 255});
|
||||
|
||||
// Level
|
||||
char levelStr[8];
|
||||
snprintf(levelStr, sizeof(levelStr), "%d", hs[i].level);
|
||||
font.draw(renderer, 500 + contentOffsetX, y, levelStr, 1.1f, SDL_Color{220, 220, 230, 255});
|
||||
|
||||
// Time
|
||||
char timeStr[16];
|
||||
int mins = int(hs[i].timeSec) / 60;
|
||||
int secs = int(hs[i].timeSec) % 60;
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", mins, secs);
|
||||
font.draw(renderer, 600 + contentOffsetX, y, timeStr, 1.1f, SDL_Color{220, 220, 230, 255});
|
||||
}
|
||||
|
||||
// Action buttons at bottom (like JS version)
|
||||
float buttonY = LOGICAL_H * 0.75f + contentOffsetY;
|
||||
char levelBtnText[32];
|
||||
snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevelSelection);
|
||||
|
||||
drawEnhancedButton(renderer, font, LOGICAL_W * 0.5f + contentOffsetX, buttonY, 200, 50, "PLAY", hoveredButton == 0);
|
||||
drawEnhancedButton(renderer, font, LOGICAL_W * 0.5f + contentOffsetX, buttonY + 60, 200, 50, levelBtnText, hoveredButton == 1);
|
||||
|
||||
// Settings icon/button (top right)
|
||||
font.draw(renderer, LOGICAL_W - 50, 20, "⚙", 1.5f, SDL_Color{200, 200, 220, 255});
|
||||
|
||||
// Draw fireworks animation
|
||||
drawFireworks(renderer);
|
||||
|
||||
// Level selection popup
|
||||
if (showLevelPopup) {
|
||||
drawLevelSelectionPopup(renderer, font, startLevelSelection);
|
||||
}
|
||||
|
||||
// Settings popup
|
||||
if (showSettingsPopup) {
|
||||
drawSettingsPopup(renderer, font, musicEnabled);
|
||||
}
|
||||
|
||||
// Footer instructions
|
||||
font.draw(renderer, 20, LOGICAL_H - 30, "F11=FULLSCREEN • L=LEVEL • S=SETTINGS • SPACE=PLAY • M=MUSIC", 1.0f, SDL_Color{150, 150, 170, 255});
|
||||
}
|
||||
break;
|
||||
// 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});
|
||||
{
|
||||
@ -1577,6 +1600,15 @@ int main(int, char **)
|
||||
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});
|
||||
|
||||
@ -4,6 +4,32 @@
|
||||
#include "../Font.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// Local logical canvas size (matches main.cpp). Kept local to avoid changing many files.
|
||||
static constexpr int LOGICAL_W = 1200;
|
||||
static constexpr int LOGICAL_H = 1000;
|
||||
|
||||
extern bool showLevelPopup; // from main
|
||||
extern bool showSettingsPopup; // from main
|
||||
extern bool musicEnabled; // from main
|
||||
extern int hoveredButton; // from main
|
||||
// Call wrappers defined in main.cpp
|
||||
extern void menu_drawFireworks(SDL_Renderer* renderer);
|
||||
extern void menu_updateFireworks(double frameMs);
|
||||
extern double menu_getLogoAnimCounter();
|
||||
extern int menu_getHoveredButton();
|
||||
extern void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
|
||||
const std::string& label, bool isHovered, bool isSelected);
|
||||
|
||||
// Menu button wrapper implemented in main.cpp
|
||||
extern 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);
|
||||
|
||||
// wrappers for popups (defined in main.cpp)
|
||||
extern void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel);
|
||||
extern void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);
|
||||
|
||||
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
|
||||
|
||||
@ -15,19 +41,125 @@ void MenuState::onExit() {
|
||||
}
|
||||
|
||||
void MenuState::handleEvent(const SDL_Event& e) {
|
||||
// Menu-specific key handling moved from main; main still handles mouse for now
|
||||
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
||||
if (ctx.startLevelSelection && *ctx.startLevelSelection >= 0) {
|
||||
// keep simple: allow L/S toggles handled globally in main for now
|
||||
}
|
||||
}
|
||||
// Key-specific handling (allow main to handle global keys)
|
||||
(void)e;
|
||||
}
|
||||
|
||||
void MenuState::update(double frameMs) {
|
||||
// Update logo animation counter and particles similar to main
|
||||
(void)frameMs;
|
||||
}
|
||||
|
||||
void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
|
||||
(void)renderer; (void)logicalScale; (void)logicalVP;
|
||||
// Main still performs actual rendering for now; this placeholder keeps the API.
|
||||
// Compute content offset using the same math as main
|
||||
float winW = float(logicalVP.w);
|
||||
float winH = float(logicalVP.h);
|
||||
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;
|
||||
|
||||
// Background is drawn by main (stretched to the full window) to avoid double-draw.
|
||||
|
||||
// Draw the animated logo and fireworks using the small logo if available (show whole image)
|
||||
SDL_Texture* logoToUse = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex;
|
||||
if (logoToUse) {
|
||||
// Use dimensions provided by the shared context when available
|
||||
int texW = (logoToUse == ctx.logoSmallTex && ctx.logoSmallW > 0) ? ctx.logoSmallW : 872;
|
||||
int texH = (logoToUse == ctx.logoSmallTex && ctx.logoSmallH > 0) ? ctx.logoSmallH : 273;
|
||||
float maxW = LOGICAL_W * 0.6f;
|
||||
float scale = std::min(1.0f, maxW / float(texW));
|
||||
float dw = texW * scale;
|
||||
float dh = texH * scale;
|
||||
float logoX = (LOGICAL_W - dw) / 2.f + contentOffsetX;
|
||||
float logoY = LOGICAL_H * 0.05f + contentOffsetY;
|
||||
SDL_FRect dst{logoX, logoY, dw, dh};
|
||||
SDL_RenderTexture(renderer, logoToUse, nullptr, &dst);
|
||||
}
|
||||
|
||||
// Fireworks (draw above high scores / near buttons)
|
||||
menu_drawFireworks(renderer);
|
||||
|
||||
// Score list and top players with a sine-wave vertical animation (use pixelFont for retro look)
|
||||
float topPlayersY = LOGICAL_H * 0.30f + contentOffsetY; // more top padding
|
||||
FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
|
||||
if (useFont) {
|
||||
useFont->draw(renderer, LOGICAL_W * 0.5f - 110 + contentOffsetX, topPlayersY, std::string("TOP PLAYERS"), 1.8f, SDL_Color{255, 220, 0, 255});
|
||||
}
|
||||
|
||||
// High scores table with wave offset
|
||||
float scoresStartY = topPlayersY + 70; // more spacing under title
|
||||
const auto &hs = ctx.scores ? ctx.scores->all() : *(new std::vector<ScoreEntry>());
|
||||
size_t maxDisplay = std::min(hs.size(), size_t(12));
|
||||
|
||||
// Draw table header
|
||||
if (useFont) {
|
||||
float cx = LOGICAL_W * 0.5f + contentOffsetX;
|
||||
float colX[] = { cx - 280, cx - 180, cx - 20, cx + 90, cx + 200, cx + 300 };
|
||||
useFont->draw(renderer, colX[0], scoresStartY - 28, "RANK", 1.1f, SDL_Color{200,200,220,255});
|
||||
useFont->draw(renderer, colX[1], scoresStartY - 28, "PLAYER", 1.1f, SDL_Color{200,200,220,255});
|
||||
useFont->draw(renderer, colX[2], scoresStartY - 28, "SCORE", 1.1f, SDL_Color{200,200,220,255});
|
||||
useFont->draw(renderer, colX[3], scoresStartY - 28, "LINES", 1.1f, SDL_Color{200,200,220,255});
|
||||
useFont->draw(renderer, colX[4], scoresStartY - 28, "LEVEL", 1.1f, SDL_Color{200,200,220,255});
|
||||
useFont->draw(renderer, colX[5], scoresStartY - 28, "TIME", 1.1f, SDL_Color{200,200,220,255});
|
||||
}
|
||||
for (size_t i = 0; i < maxDisplay; ++i)
|
||||
{
|
||||
float baseY = scoresStartY + i * 25;
|
||||
float wave = std::sin((float)menu_getLogoAnimCounter() * 0.006f + i * 0.25f) * 6.0f; // subtle wave
|
||||
float y = baseY + wave;
|
||||
|
||||
// Center columns around mid X, wider
|
||||
float cx = LOGICAL_W * 0.5f + contentOffsetX;
|
||||
float colX[] = { cx - 280, cx - 180, cx - 20, cx + 90, cx + 200, cx + 300 };
|
||||
|
||||
char rankStr[8];
|
||||
std::snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1);
|
||||
if (useFont) useFont->draw(renderer, colX[0], y, rankStr, 1.0f, SDL_Color{220, 220, 230, 255});
|
||||
|
||||
if (useFont) useFont->draw(renderer, colX[1], y, hs[i].name, 1.0f, SDL_Color{220, 220, 230, 255});
|
||||
|
||||
char scoreStr[16]; std::snprintf(scoreStr, sizeof(scoreStr), "%d", hs[i].score);
|
||||
if (useFont) useFont->draw(renderer, colX[2], y, scoreStr, 1.0f, SDL_Color{220, 220, 230, 255});
|
||||
|
||||
char linesStr[8]; std::snprintf(linesStr, sizeof(linesStr), "%d", hs[i].lines);
|
||||
if (useFont) useFont->draw(renderer, colX[3], y, linesStr, 1.0f, SDL_Color{220, 220, 230, 255});
|
||||
|
||||
char levelStr[8]; std::snprintf(levelStr, sizeof(levelStr), "%d", hs[i].level);
|
||||
if (useFont) useFont->draw(renderer, colX[4], y, levelStr, 1.0f, SDL_Color{220, 220, 230, 255});
|
||||
|
||||
char timeStr[16]; int mins = int(hs[i].timeSec) / 60; int secs = int(hs[i].timeSec) % 60;
|
||||
std::snprintf(timeStr, sizeof(timeStr), "%d:%02d", mins, secs);
|
||||
if (useFont) useFont->draw(renderer, colX[5], y, timeStr, 1.0f, SDL_Color{220, 220, 230, 255});
|
||||
}
|
||||
|
||||
// Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test)
|
||||
bool isSmall = (contentW < 700.0f);
|
||||
float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f;
|
||||
float btnH = isSmall ? 60.0f : 70.0f;
|
||||
float btnX = LOGICAL_W * 0.5f + contentOffsetX;
|
||||
// Move buttons down by 40px to match original layout (user requested 30-50px)
|
||||
const float btnYOffset = 40.0f;
|
||||
float btnY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; // align with main's button vertical position
|
||||
|
||||
if (ctx.pixelFont) {
|
||||
char levelBtnText[32];
|
||||
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
|
||||
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
||||
// left = green, right = blue like original
|
||||
menu_drawMenuButton(renderer, *ctx.pixelFont, btnX - btnW * 0.6f, btnY, btnW, btnH, std::string("PLAY"), SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255});
|
||||
menu_drawMenuButton(renderer, *ctx.pixelFont, btnX + btnW * 0.6f, btnY, btnW, btnH, std::string(levelBtnText), SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255});
|
||||
}
|
||||
|
||||
// Popups (level/settings) if requested
|
||||
if (ctx.showLevelPopup && *ctx.showLevelPopup) {
|
||||
// call wrapper which will internally draw on top of current content
|
||||
// prefer pixelFont for retro look
|
||||
FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
|
||||
menu_drawLevelSelectionPopup(renderer, *useFont, ctx.backgroundTex, ctx.startLevelSelection ? *ctx.startLevelSelection : 0);
|
||||
}
|
||||
if (ctx.showSettingsPopup && *ctx.showSettingsPopup) {
|
||||
menu_drawSettingsPopup(renderer, *ctx.font, ctx.musicEnabled ? *ctx.musicEnabled : false);
|
||||
}
|
||||
}
|
||||
|
||||
52
src/states/PlayingState.cpp
Normal file
52
src/states/PlayingState.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
#include "PlayingState.h"
|
||||
#include "../Game.h"
|
||||
#include "../LineEffect.h"
|
||||
#include "../Scores.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
PlayingState::PlayingState(StateContext& ctx) : State(ctx) {}
|
||||
|
||||
void PlayingState::onEnter() {
|
||||
// Nothing yet; main still owns game creation
|
||||
}
|
||||
|
||||
void PlayingState::onExit() {
|
||||
}
|
||||
|
||||
void PlayingState::handleEvent(const SDL_Event& e) {
|
||||
// We keep short-circuited input here; main still handles mouse UI
|
||||
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
||||
if (!ctx.game) return;
|
||||
// Pause toggle (P)
|
||||
if (e.key.scancode == SDL_SCANCODE_P) {
|
||||
bool paused = ctx.game->isPaused();
|
||||
ctx.game->setPaused(!paused);
|
||||
}
|
||||
// Other gameplay keys already registered by main's Playing handler for now
|
||||
}
|
||||
}
|
||||
|
||||
void PlayingState::update(double frameMs) {
|
||||
if (!ctx.game) return;
|
||||
// forward per-frame gameplay updates (gravity, elapsed)
|
||||
if (!ctx.game->isPaused()) {
|
||||
ctx.game->tickGravity(frameMs);
|
||||
ctx.game->addElapsed(frameMs);
|
||||
|
||||
if (ctx.lineEffect && ctx.lineEffect->isActive()) {
|
||||
if (ctx.lineEffect->update(frameMs / 1000.0f)) {
|
||||
ctx.game->clearCompletedLines();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.game->isGameOver()) {
|
||||
if (ctx.scores) ctx.scores->submit(ctx.game->score(), ctx.game->lines(), ctx.game->level(), ctx.game->elapsed());
|
||||
// Transitioning state must be done by the owner (main via StateManager hooks). We can't set state here.
|
||||
}
|
||||
}
|
||||
|
||||
void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
|
||||
if (!ctx.game) return;
|
||||
// Rendering kept in main for now to avoid changing many layout calculations in one change.
|
||||
}
|
||||
17
src/states/PlayingState.h
Normal file
17
src/states/PlayingState.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include "State.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
class PlayingState : public State {
|
||||
public:
|
||||
PlayingState(StateContext& ctx);
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void handleEvent(const SDL_Event& e) override;
|
||||
void update(double frameMs) override;
|
||||
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
|
||||
|
||||
private:
|
||||
// Local per-state variables if needed
|
||||
bool localPaused = false;
|
||||
};
|
||||
@ -25,6 +25,9 @@ struct StateContext {
|
||||
|
||||
// Textures
|
||||
SDL_Texture* logoTex = nullptr;
|
||||
SDL_Texture* logoSmallTex = nullptr;
|
||||
int logoSmallW = 0;
|
||||
int logoSmallH = 0;
|
||||
SDL_Texture* backgroundTex = nullptr;
|
||||
SDL_Texture* blocksTex = nullptr;
|
||||
|
||||
@ -33,6 +36,9 @@ struct StateContext {
|
||||
bool* musicEnabled = nullptr;
|
||||
int* startLevelSelection = nullptr;
|
||||
int* hoveredButton = nullptr;
|
||||
// Menu popups (exposed from main)
|
||||
bool* showLevelPopup = nullptr;
|
||||
bool* showSettingsPopup = nullptr;
|
||||
};
|
||||
|
||||
class State {
|
||||
|
||||
Reference in New Issue
Block a user