Updated game structure
Some checks failed
Build and Package Tetris / build-windows (push) Has been cancelled
Build and Package Tetris / build-linux (push) Has been cancelled

This commit is contained in:
2025-08-16 12:10:19 +02:00
parent 71648fbaeb
commit 2afaea7fd3
36 changed files with 665 additions and 382 deletions

View File

@ -1,77 +0,0 @@
// Starfield3D.h - 3D Parallax Starfield Effect
// Creates a parallax starfield effect that simulates 3D space movement
// By projecting 2D coordinates into 3D space with perspective
#pragma once
#include <SDL3/SDL.h>
#include <vector>
#include <random>
class Starfield3D {
public:
Starfield3D();
~Starfield3D() = default;
// Initialize the starfield with dimensions
void init(int width, int height, int starCount = 160);
// Update starfield animation (call every frame)
void update(float deltaTime);
// Draw the starfield
void draw(SDL_Renderer* renderer);
// Update dimensions when window resizes
void resize(int width, int height);
private:
// Star representation in 3D space
struct Star3D {
float x, y, z; // 3D position
float vx, vy, vz; // Current velocities
float targetVx, targetVy, targetVz; // Target velocities for smooth transitions
bool changing; // Whether star is currently changing direction
float changeTimer; // Timer for direction change duration
int type; // Star type (determines color/brightness)
Star3D() : x(0), y(0), z(0), vx(0), vy(0), vz(0),
targetVx(0), targetVy(0), targetVz(0),
changing(false), changeTimer(0), type(0) {}
};
// Configuration constants
static constexpr float MAX_DEPTH = 32.0f;
static constexpr float STAR_SPEED = 0.4f;
static constexpr float DEPTH_FACTOR = 256.0f;
static constexpr float MIN_Z = 0.1f;
static constexpr float MAX_Z = 50.0f;
static constexpr float DIRECTION_CHANGE_PROBABILITY = 0.008f;
static constexpr float MAX_VELOCITY = 0.3f;
static constexpr float VELOCITY_CHANGE = 0.03f;
static constexpr float REVERSE_PROBABILITY = 0.4f;
// Private methods
void createStarfield();
void updateStar(int index);
void setRandomDirection(Star3D& star);
float randomFloat(float min, float max);
int randomRange(int min, int max);
void drawStar(SDL_Renderer* renderer, float x, float y, int type);
// Member variables
std::vector<Star3D> stars;
std::mt19937 rng;
int width, height;
float centerX, centerY;
// Star colors (RGB values)
static constexpr SDL_Color STAR_COLORS[] = {
{255, 255, 255, 255}, // White
{170, 170, 170, 255}, // Light gray
{153, 153, 153, 255}, // Medium gray
{119, 119, 119, 255}, // Dark gray
{85, 85, 85, 255} // Very dark gray
};
static constexpr int COLOR_COUNT = 5;
};

View File

@ -1,57 +0,0 @@
// StateManager.h - typed app state router with lifecycle hooks
#pragma once
#include <functional>
#include <unordered_map>
#include <SDL3/SDL.h>
enum class AppState {
Loading = 0,
Menu = 1,
LevelSelect = 2,
Playing = 3,
GameOver = 4,
Settings = 5
};
class StateManager {
public:
using EventHandler = std::function<void(const SDL_Event&)>;
using LifecycleHook = std::function<void()>;
explicit StateManager(AppState initial = AppState::Loading) : current(initial) {}
void registerHandler(AppState state, EventHandler handler) {
handlers[static_cast<int>(state)] = std::move(handler);
}
void registerOnEnter(AppState state, LifecycleHook hook) {
onEnterHooks[static_cast<int>(state)] = std::move(hook);
}
void registerOnExit(AppState state, LifecycleHook hook) {
onExitHooks[static_cast<int>(state)] = std::move(hook);
}
void setState(AppState state) {
// Call exit hook for current state
auto it = onExitHooks.find(static_cast<int>(current));
if (it != onExitHooks.end() && it->second) it->second();
current = state;
// Call enter hook for new state
auto it2 = onEnterHooks.find(static_cast<int>(current));
if (it2 != onEnterHooks.end() && it2->second) it2->second();
}
AppState getState() const { return current; }
void handleEvent(const SDL_Event& e) const {
auto it = handlers.find(static_cast<int>(current));
if (it != handlers.end() && it->second) it->second(e);
}
private:
AppState current;
std::unordered_map<int, EventHandler> handlers;
std::unordered_map<int, LifecycleHook> onEnterHooks;
std::unordered_map<int, LifecycleHook> onExitHooks;
};

View File

@ -1,5 +1,5 @@
// Audio.cpp - Windows Media Foundation MP3 decoding
#include "Audio.h"
#include "audio/Audio.h"
#include <SDL3/SDL.h>
#include <cstdio>
#include <algorithm>
@ -26,7 +26,7 @@ Audio& Audio::instance(){ static Audio inst; return inst; }
bool Audio::init(){ if(outSpec.freq!=0) return true; outSpec.format=SDL_AUDIO_S16; outSpec.channels=outChannels; outSpec.freq=outRate;
#ifdef _WIN32
if(!mfStarted){ if(FAILED(MFStartup(MF_VERSION))) { std::fprintf(stderr,"[Audio] MFStartup failed\n"); } else mfStarted=true; }
if(!mfStarted){ if(FAILED(MFStartup(MF_VERSION))) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MFStartup failed"); } else mfStarted=true; }
#endif
return true; }
@ -45,9 +45,9 @@ static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int
void Audio::addTrack(const std::string& path){ AudioTrack t; t.path=path;
#ifdef _WIN32
if(decodeMP3(path, t.pcm, t.rate, t.channels)) t.ok=true; else std::fprintf(stderr,"[Audio] Failed to decode %s\n", path.c_str());
if(decodeMP3(path, t.pcm, t.rate, t.channels)) t.ok=true; else SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode %s", path.c_str());
#else
std::fprintf(stderr,"[Audio] MP3 unsupported on this platform (stub): %s\n", path.c_str());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported on this platform (stub): %s", path.c_str());
#endif
tracks.push_back(std::move(t)); }
@ -60,7 +60,7 @@ bool Audio::ensureStream(){
if(audioStream) return true;
audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &outSpec, &Audio::streamCallback, this);
if(!audioStream){
std::fprintf(stderr,"[Audio] SDL_OpenAudioDeviceStream failed: %s\n", SDL_GetError());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] SDL_OpenAudioDeviceStream failed: %s", SDL_GetError());
return false;
}
return true;
@ -170,7 +170,7 @@ void Audio::backgroundLoadingThread() {
bool mfInitialized = SUCCEEDED(hrMF);
if (!mfInitialized) {
std::fprintf(stderr, "[Audio] Failed to initialize MF on background thread\n");
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to initialize MF on background thread");
}
#endif
@ -188,10 +188,10 @@ void Audio::backgroundLoadingThread() {
if (mfInitialized && decodeMP3(path, t.pcm, t.rate, t.channels)) {
t.ok = true;
} else {
std::fprintf(stderr, "[Audio] Failed to decode %s\n", path.c_str());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode %s", path.c_str());
}
#else
std::fprintf(stderr, "[Audio] MP3 unsupported on this platform (stub): %s\n", path.c_str());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported on this platform (stub): %s", path.c_str());
#endif
// Thread-safe addition to tracks

View File

@ -9,7 +9,14 @@
#include <mutex>
#include <atomic>
struct AudioTrack { std::string path; std::vector<int16_t> pcm; int channels=2; int rate=44100; size_t cursor=0; bool ok=false; };
struct AudioTrack {
std::string path;
std::vector<int16_t> pcm;
int channels = 2;
int rate = 44100;
size_t cursor = 0;
bool ok = false;
};
class Audio {
public:

22
src/audio/MenuWrappers.h Normal file
View File

@ -0,0 +1,22 @@
// MenuWrappers.h - function prototypes for menu helper wrappers implemented in main.cpp
#pragma once
#include <SDL3/SDL.h>
#include <string>
class FontAtlas;
// Draw fireworks using the provided blocks texture (may be nullptr)
void menu_drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex);
void menu_updateFireworks(double frameMs);
double menu_getLogoAnimCounter();
int menu_getHoveredButton();
void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, bool isHovered, bool isSelected);
void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, SDL_Color bgColor, SDL_Color borderColor);
void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel);
void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);

View File

@ -1,7 +1,7 @@
// SoundEffect.cpp - Implementation of sound effects system
#include "SoundEffect.h"
#include <SDL3/SDL.h>
#include "Audio.h"
#include "audio/Audio.h"
#include <cstdio>
#include <algorithm>
#include <random>
@ -34,7 +34,7 @@ bool SoundEffect::load(const std::string& filePath) {
} else if (extension == "mp3") {
success = loadMP3(filePath);
} else {
std::fprintf(stderr, "[SoundEffect] Unsupported file format: %s\n", extension.c_str());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Unsupported file format: %s", extension.c_str());
return false;
}
@ -49,7 +49,7 @@ bool SoundEffect::load(const std::string& filePath) {
void SoundEffect::play(float volume) {
if (!loaded || pcmData.empty()) {
std::printf("[SoundEffect] Cannot play - loaded=%d, pcmData.size()=%zu\n", loaded, pcmData.size());
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Cannot play - loaded=%d, pcmData.size()=%zu", loaded, pcmData.size());
return;
}
@ -102,7 +102,7 @@ bool SoundEffect::loadWAV(const std::string& filePath) {
Uint32 wavLength;
if (!SDL_LoadWAV(filePath.c_str(), &wavSpec, &wavBuffer, &wavLength)) {
std::fprintf(stderr, "[SoundEffect] Failed to load WAV file %s: %s\n",
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Failed to load WAV file %s: %s",
filePath.c_str(), SDL_GetError());
return false;
}
@ -147,7 +147,7 @@ bool SoundEffect::loadMP3(const std::string& filePath) {
static bool mfInitialized = false;
if (!mfInitialized) {
if (FAILED(MFStartup(MF_VERSION))) {
std::fprintf(stderr, "[SoundEffect] MFStartup failed\n");
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] MFStartup failed");
return false;
}
mfInitialized = true;
@ -157,12 +157,12 @@ bool SoundEffect::loadMP3(const std::string& filePath) {
wchar_t wpath[MAX_PATH];
int wlen = MultiByteToWideChar(CP_UTF8, 0, filePath.c_str(), -1, wpath, MAX_PATH);
if (!wlen) {
std::fprintf(stderr, "[SoundEffect] Failed to convert path to wide char\n");
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Failed to convert path to wide char");
return false;
}
if (FAILED(MFCreateSourceReaderFromURL(wpath, nullptr, &reader))) {
std::fprintf(stderr, "[SoundEffect] Failed to create source reader for %s\n", filePath.c_str());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Failed to create source reader for %s", filePath.c_str());
return false;
}
@ -214,7 +214,7 @@ bool SoundEffect::loadMP3(const std::string& filePath) {
}
if (tempData.empty()) {
std::fprintf(stderr, "[SoundEffect] No audio data decoded from %s\n", filePath.c_str());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] No audio data decoded from %s", filePath.c_str());
return false;
}
@ -223,7 +223,7 @@ bool SoundEffect::loadMP3(const std::string& filePath) {
sampleRate = 44100;
return true;
#else
std::fprintf(stderr, "[SoundEffect] MP3 support not available on this platform\n");
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] MP3 support not available on this platform");
return false;
#endif
}

View File

@ -0,0 +1,36 @@
#include "GravityManager.h"
#include <algorithm>
#include <iostream>
GravityManager::GravityManager() {
levelMultipliers.fill(1.0);
}
void GravityManager::setGlobalMultiplier(double m) {
globalMultiplier = std::clamp(m, 0.01, 100.0);
}
double GravityManager::getGlobalMultiplier() const { return globalMultiplier; }
void GravityManager::setLevelMultiplier(int level, double m) {
if (level < 0) return;
int idx = level >= 29 ? 29 : level;
levelMultipliers[idx] = std::clamp(m, 0.01, 100.0);
}
double GravityManager::getLevelMultiplier(int level) const {
int idx = level < 0 ? 0 : (level >= 29 ? 29 : level);
return levelMultipliers[idx];
}
double GravityManager::getMsForLevel(int level) const {
int idx = level < 0 ? 0 : (level >= 29 ? 29 : level);
double frames = static_cast<double>(FRAMES_TABLE[idx]) * levelMultipliers[idx];
double result = frames * FRAME_MS * globalMultiplier;
return std::max(1.0, result);
}
double GravityManager::getFpsForLevel(int level) const {
double ms = getMsForLevel(level);
return 1000.0 / ms;
}

31
src/core/GravityManager.h Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include <array>
class GravityManager {
public:
GravityManager();
// Global multiplier applied to all levels
void setGlobalMultiplier(double m);
double getGlobalMultiplier() const;
// Per-level multiplier (29 = 29+)
void setLevelMultiplier(int level, double m);
double getLevelMultiplier(int level) const;
// Compute ms per cell and fps for a given level
double getMsForLevel(int level) const;
double getFpsForLevel(int level) const;
private:
static constexpr double NES_FPS = 60.0988;
static constexpr double FRAME_MS = 1000.0 / NES_FPS;
static constexpr int FRAMES_TABLE[30] = {
48,43,38,33,28,23,18,13,8,6,
5,5,5,4,4,4,3,3,3,2,
2,2,2,2,2,2,2,2,2,1
};
double globalMultiplier{1.0};
std::array<double,30> levelMultipliers{}; // default 1.0
};

46
src/core/StateManager.cpp Normal file
View File

@ -0,0 +1,46 @@
#include "StateManager.h"
StateManager::StateManager(AppState initial)
: currentState(initial)
{
}
void StateManager::registerHandler(AppState s, EventHandler h) {
handlers[static_cast<int>(s)].push_back(std::move(h));
}
void StateManager::registerOnEnter(AppState s, Hook h) {
onEnter[static_cast<int>(s)].push_back(std::move(h));
}
void StateManager::registerOnExit(AppState s, Hook h) {
onExit[static_cast<int>(s)].push_back(std::move(h));
}
// Overload accepting a no-arg function as handler (wraps it into an EventHandler)
void StateManager::registerHandler(AppState s, std::function<void()> h) {
EventHandler wrapper = [h = std::move(h)](const SDL_Event&) { h(); };
registerHandler(s, std::move(wrapper));
}
void StateManager::setState(AppState s) {
if (s == currentState) return;
// call exit hooks for current
auto it = onExit.find(static_cast<int>(currentState));
if (it != onExit.end()) {
for (auto &h : it->second) h();
}
currentState = s;
auto it2 = onEnter.find(static_cast<int>(currentState));
if (it2 != onEnter.end()) {
for (auto &h : it2->second) h();
}
}
AppState StateManager::getState() const { return currentState; }
void StateManager::handleEvent(const SDL_Event& e) {
auto it = handlers.find(static_cast<int>(currentState));
if (it == handlers.end()) return;
for (auto &h : it->second) h(e);
}

41
src/core/StateManager.h Normal file
View File

@ -0,0 +1,41 @@
#pragma once
#include <functional>
#include <unordered_map>
#include <vector>
#include <SDL3/SDL.h>
// Application states used across the app
enum class AppState {
Loading,
Menu,
Playing,
LevelSelect,
GameOver
};
// State manager used by main to route events and lifecycle hooks
class StateManager {
public:
using EventHandler = std::function<void(const SDL_Event&)>;
using Hook = std::function<void()>;
StateManager(AppState initial);
void registerHandler(AppState s, EventHandler h);
void registerOnEnter(AppState s, Hook h);
void registerOnExit(AppState s, Hook h);
void registerHandler(AppState s, std::function<void()> h); // overload used in some places
void setState(AppState s);
AppState getState() const;
void handleEvent(const SDL_Event& e);
private:
AppState currentState;
std::unordered_map<int, std::vector<EventHandler>> handlers;
std::unordered_map<int, std::vector<Hook>> onEnter;
std::unordered_map<int, std::vector<Hook>> onExit;
};

View File

@ -1,5 +1,5 @@
// Game.cpp - Implementation of core Tetris game logic
#include "Game.h"
#include "gameplay/Game.h"
#include <algorithm>
#include <cmath>

View File

@ -1,3 +1,4 @@
// Game.h - Core Tetris game logic (board, piece mechanics, scoring events only)
#pragma once
#include <array>
@ -5,6 +6,8 @@
#include <random>
#include <cstdint>
#include <functional>
#include <memory>
#include "core/GravityManager.h"
enum PieceType { I, O, T, S, Z, J, L, PIECE_COUNT };
using Shape = std::array<uint16_t, 4>; // four rotation bitmasks
@ -96,6 +99,10 @@ private:
// Gravity tuning -----------------------------------------------------
// Global multiplier applied to all level timings (use to slow/speed whole-game gravity)
double gravityGlobalMultiplier{2.8};
// Gravity manager encapsulates frames table, multipliers and conversions
GravityManager gravityMgr;
// Backwards-compatible accessors (delegate to gravityMgr)
double computeGravityMsForLevel(int level) const;
// Internal helpers ----------------------------------------------------
void refillBag();

View File

@ -274,7 +274,7 @@ void LineEffect::playLineClearSound(int lineCount) {
audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
if (!audioStream) {
printf("Warning: Could not create audio stream for line clear effects\n");
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Could not create audio stream for line clear effects");
return;
}
}

View File

@ -45,15 +45,15 @@ public:
bool isActive() const { return state != AnimationState::IDLE; }
private:
SDL_Renderer* renderer;
AnimationState state;
float timer;
SDL_Renderer* renderer{nullptr};
AnimationState state{AnimationState::IDLE};
float timer{0.0f};
std::vector<int> clearingRows;
std::vector<Particle> particles;
std::mt19937 rng;
std::mt19937 rng{std::random_device{}()};
// Audio resources
SDL_AudioStream* audioStream;
SDL_AudioStream* audioStream{nullptr};
std::vector<int16_t> lineClearSample;
std::vector<int16_t> tetrisSample;

View File

@ -1,5 +1,5 @@
// Font.cpp - implementation of FontAtlas
#include "Font.h"
// Font.cpp - implementation of FontAtlas (copied into src/graphics)
#include "graphics/Font.h"
#include <SDL3/SDL.h>
bool FontAtlas::init(const std::string& path, int basePt) { fontPath = path; baseSize = basePt; return true; }

View File

@ -1,5 +1,5 @@
// Starfield.cpp - implementation
#include "Starfield.h"
// Starfield.cpp - implementation (copied into src/graphics)
#include "graphics/Starfield.h"
#include <SDL3/SDL.h>
#include <random>

View File

@ -0,0 +1,63 @@
// Starfield3D.h - 3D Parallax Starfield Effect (canonical)
#pragma once
#include <SDL3/SDL.h>
#include <vector>
#include <random>
#include <array>
class Starfield3D {
public:
Starfield3D();
~Starfield3D() = default;
void init(int width, int height, int starCount = 160);
void update(float deltaTime);
void draw(SDL_Renderer* renderer);
void resize(int width, int height);
private:
struct Star3D {
float x, y, z;
float vx, vy, vz;
float targetVx, targetVy, targetVz;
float changeTimer;
bool changing;
int type;
};
// Helpers used by the implementation
void createStarfield();
void updateStar(int index);
void setRandomDirection(Star3D& star);
float randomFloat(float min, float max);
int randomRange(int min, int max);
void drawStar(SDL_Renderer* renderer, float x, float y, int type);
std::vector<Star3D> stars;
int width{0}, height{0};
float centerX{0}, centerY{0};
// Random number generator
std::mt19937 rng;
// Visual / behavioral constants (tweakable)
inline static constexpr float MAX_VELOCITY = 0.5f;
inline static constexpr float REVERSE_PROBABILITY = 0.12f;
inline static constexpr float STAR_SPEED = 0.6f;
inline static constexpr float MAX_DEPTH = 120.0f;
inline static constexpr float DIRECTION_CHANGE_PROBABILITY = 0.002f;
inline static constexpr float VELOCITY_CHANGE = 0.02f;
inline static constexpr float MIN_Z = 0.1f;
inline static constexpr float MAX_Z = MAX_DEPTH;
inline static constexpr float DEPTH_FACTOR = 320.0f;
inline static constexpr int COLOR_COUNT = 5;
inline static const std::array<SDL_Color, COLOR_COUNT> STAR_COLORS = {
SDL_Color{255,255,255,255},
SDL_Color{200,200,255,255},
SDL_Color{255,220,180,255},
SDL_Color{180,220,255,255},
SDL_Color{255,180,200,255}
};
};

View File

@ -14,19 +14,20 @@
#include <cstdlib>
#include <memory>
#include "Audio.h"
#include "SoundEffect.h"
#include "audio/Audio.h"
#include "audio/SoundEffect.h"
#include "Game.h"
#include "Scores.h"
#include "Starfield.h"
#include "gameplay/Game.h"
#include "persistence/Scores.h"
#include "graphics/Starfield.h"
#include "Starfield3D.h"
#include "Font.h"
#include "LineEffect.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)
@ -331,7 +332,7 @@ static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musi
// Starfield now managed by Starfield class
// State manager integration (scaffolded in StateManager.h)
#include "StateManager.h"
#include "core/StateManager.h"
// -----------------------------------------------------------------------------
// Intro/Menu state variables
@ -341,8 +342,6 @@ 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 (block particles)
@ -389,44 +388,7 @@ struct TetrisFirework {
}
return !particles.empty();
}
void draw(SDL_Renderer* renderer) {
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);
}
}
}
// Drawing is handled by drawFireworks_impl which accepts the texture to use.
};
static std::vector<TetrisFirework> fireworks;
@ -455,13 +417,41 @@ static void updateFireworks(double frameMs) {
}
}
static void drawFireworks(SDL_Renderer* renderer) {
for (auto& f : fireworks) f.draw(renderer);
// 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)
// These call the internal helpers above so we don't change existing static linkage.
void menu_drawFireworks(SDL_Renderer* renderer) { drawFireworks(renderer); }
// 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; }
@ -474,20 +464,20 @@ int main(int, char **)
int sdlInitRes = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
if (sdlInitRes < 0)
{
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed: %s", SDL_GetError());
return 1;
}
int ttfInitRes = TTF_Init();
if (ttfInitRes < 0)
{
std::fprintf(stderr, "TTF_Init failed\n");
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)
{
std::fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow failed: %s", SDL_GetError());
TTF_Quit();
SDL_Quit();
return 1;
@ -495,7 +485,7 @@ int main(int, char **)
SDL_Renderer *renderer = SDL_CreateRenderer(window, nullptr);
if (!renderer)
{
std::fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateRenderer failed: %s", SDL_GetError());
SDL_DestroyWindow(window);
TTF_Quit();
SDL_Quit();
@ -556,6 +546,9 @@ int main(int, char **)
} 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;
@ -575,6 +568,7 @@ int main(int, char **)
} else {
(void)0;
}
// No global exposure of blocksTex; states receive textures via StateContext.
if (!blocksTex) {
(void)0;
@ -613,8 +607,7 @@ int main(int, char **)
(void)0;
}
// Provide the blocks sheet to the fireworks system (for block particles)
fireworksBlocksTex = blocksTex;
// Provide the blocks sheet to the fireworks system through StateContext (no globals).
// Default start level selection: 0
int startLevelSelection = 0;
@ -656,7 +649,7 @@ int main(int, char **)
}
}
std::fprintf(stderr, "Failed to load sound: %s (tried both WAV and MP3)\n", id.c_str());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load sound: %s (tried both WAV and MP3)", id.c_str());
};
loadSoundWithFallback("nice_combo", "nice_combo");

View File

@ -1,4 +1,4 @@
// Scores.cpp - Implementation of ScoreManager
// Scores.cpp - Implementation of ScoreManager (copied into src/persistence)
#include "Scores.h"
#include <SDL3/SDL.h>
#include <fstream>

View File

@ -1,6 +1,6 @@
// LoadingState.cpp
#include "LoadingState.h"
#include "../Game.h"
#include "gameplay/Game.h"
#include <SDL3/SDL.h>
#include <cstdio>

View File

@ -1,7 +1,7 @@
// MenuState.cpp
#include "MenuState.h"
#include "../Scores.h"
#include "../Font.h"
#include "persistence/Scores.h"
#include "graphics/Font.h"
#include <SDL3/SDL.h>
#include <cstdio>
#include <algorithm>
@ -11,25 +11,11 @@
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);
// Shared flags and resources are provided via StateContext `ctx`.
// Removed fragile extern declarations and use `ctx.showLevelPopup`, `ctx.showSettingsPopup`,
// `ctx.musicEnabled` and `ctx.hoveredButton` instead to avoid globals.
// Menu helper wrappers are declared in a shared header implemented in main.cpp
#include "../audio/MenuWrappers.h"
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
@ -79,7 +65,7 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
}
// Fireworks (draw above high scores / near buttons)
menu_drawFireworks(renderer);
menu_drawFireworks(renderer, ctx.blocksTex);
// 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

View File

@ -1,7 +1,7 @@
#include "PlayingState.h"
#include "../Game.h"
#include "../LineEffect.h"
#include "../Scores.h"
#include "gameplay/Game.h"
#include "gameplay/LineEffect.h"
#include "persistence/Scores.h"
#include <SDL3/SDL.h>
PlayingState::PlayingState(StateContext& ctx) : State(ctx) {}

View File

@ -29,6 +29,8 @@ struct StateContext {
int logoSmallW = 0;
int logoSmallH = 0;
SDL_Texture* backgroundTex = nullptr;
// backgroundTex is set once in `main.cpp` and passed to states via this context.
// Prefer reading this field instead of relying on any `extern SDL_Texture*` globals.
SDL_Texture* blocksTex = nullptr;
// Audio / SFX - forward declared types in main

View File

@ -1,17 +0,0 @@
{
"folders": [
{
"path": ".."
},
{
"path": "../../../games/Tetris"
}
],
"settings": {
"workbench.colorCustomizations": {
"activityBar.background": "#59140D",
"titleBar.activeBackground": "#7D1D12",
"titleBar.activeForeground": "#FFFCFC"
}
}
}

22
src/ui/MenuWrappers.h Normal file
View File

@ -0,0 +1,22 @@
// MenuWrappers.h - function prototypes for menu helper wrappers implemented in main.cpp
#pragma once
#include <SDL3/SDL.h>
#include <string>
class FontAtlas;
// Draw fireworks using the provided blocks texture (may be nullptr)
void menu_drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex);
void menu_updateFireworks(double frameMs);
double menu_getLogoAnimCounter();
int menu_getHoveredButton();
void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, bool isHovered, bool isSelected);
void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
const std::string& label, SDL_Color bgColor, SDL_Color borderColor);
void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel);
void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);