some problems fixed
This commit is contained in:
@ -41,6 +41,7 @@ add_executable(tetris
|
||||
src/graphics/Starfield.cpp
|
||||
src/graphics/Starfield3D.cpp
|
||||
src/graphics/Font.cpp
|
||||
src/graphics/GameRenderer.cpp
|
||||
src/audio/Audio.cpp
|
||||
src/gameplay/LineEffect.cpp
|
||||
src/audio/SoundEffect.cpp
|
||||
@ -134,6 +135,7 @@ add_executable(tetris_refactored
|
||||
src/graphics/Starfield.cpp
|
||||
src/graphics/Starfield3D.cpp
|
||||
src/graphics/Font.cpp
|
||||
src/graphics/GameRenderer.cpp
|
||||
src/audio/Audio.cpp
|
||||
src/gameplay/LineEffect.cpp
|
||||
src/audio/SoundEffect.cpp
|
||||
|
||||
5
check_events.cpp
Normal file
5
check_events.cpp
Normal file
@ -0,0 +1,5 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <iostream>
|
||||
int main() { std::cout << \
|
||||
SDL_EVENT_QUIT:
|
||||
\ << SDL_EVENT_QUIT << std::endl; return 0; }
|
||||
@ -63,14 +63,33 @@ bool Audio::ensureStream(){
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] SDL_OpenAudioDeviceStream failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
// Ensure the device is running so SFX can be heard even before music starts
|
||||
SDL_ResumeAudioStreamDevice(audioStream);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Audio::start(){ if(!ensureStream()) return; if(!playing){ current=-1; nextTrack(); SDL_ResumeAudioStreamDevice(audioStream); playing=true; } }
|
||||
void Audio::start(){
|
||||
if(!ensureStream()) return;
|
||||
// If no track is selected yet, try to select one now (in case tracks loaded after initial start)
|
||||
if(current < 0) {
|
||||
nextTrack();
|
||||
}
|
||||
SDL_ResumeAudioStreamDevice(audioStream);
|
||||
playing = true;
|
||||
}
|
||||
|
||||
void Audio::toggleMute(){ muted=!muted; }
|
||||
|
||||
void Audio::nextTrack(){ if(tracks.empty()) return; for(size_t i=0;i<tracks.size(); ++i){ current = (current + 1) % (int)tracks.size(); if(tracks[current].ok){ tracks[current].cursor=0; return; } } current=-1; }
|
||||
void Audio::nextTrack(){
|
||||
if(tracks.empty()) { current = -1; return; }
|
||||
// Try every track once to find a decodable one
|
||||
int start = current;
|
||||
for(size_t i=0;i<tracks.size(); ++i){
|
||||
current = (current + 1) % (int)tracks.size();
|
||||
if(tracks[current].ok){ tracks[current].cursor=0; return; }
|
||||
}
|
||||
current=-1;
|
||||
}
|
||||
|
||||
void Audio::feed(Uint32 bytesWanted, SDL_AudioStream* stream){
|
||||
if(bytesWanted==0) return;
|
||||
@ -156,7 +175,15 @@ void Audio::addTrackAsync(const std::string& path) {
|
||||
}
|
||||
|
||||
void Audio::startBackgroundLoading() {
|
||||
if (loadingThread.joinable()) return; // Already running
|
||||
// If a previous loading thread exists but has finished, join it so we can start anew
|
||||
if (loadingThread.joinable()) {
|
||||
if (loadingComplete) {
|
||||
loadingThread.join();
|
||||
} else {
|
||||
// Already running
|
||||
return;
|
||||
}
|
||||
}
|
||||
loadingComplete = false;
|
||||
loadedCount = 0;
|
||||
loadingThread = std::thread(&Audio::backgroundLoadingThread, this);
|
||||
@ -174,14 +201,14 @@ void Audio::backgroundLoadingThread() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Copy pending tracks to avoid holding the mutex during processing
|
||||
std::vector<std::string> tracksToProcess;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pendingTracksMutex);
|
||||
tracksToProcess = pendingTracks;
|
||||
}
|
||||
|
||||
for (const std::string& path : tracksToProcess) {
|
||||
while (true) {
|
||||
std::string path;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pendingTracksMutex);
|
||||
if (pendingTracks.empty()) break;
|
||||
path = std::move(pendingTracks.front());
|
||||
pendingTracks.erase(pendingTracks.begin());
|
||||
}
|
||||
AudioTrack t;
|
||||
t.path = path;
|
||||
#ifdef _WIN32
|
||||
@ -200,7 +227,7 @@ void Audio::backgroundLoadingThread() {
|
||||
tracks.push_back(std::move(t));
|
||||
}
|
||||
|
||||
loadedCount++;
|
||||
loadedCount++;
|
||||
|
||||
// Small delay to prevent overwhelming the system
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
#include "../graphics/Font.h"
|
||||
#include "../graphics/Starfield3D.h"
|
||||
#include "../graphics/Starfield.h"
|
||||
#include "../graphics/GameRenderer.h"
|
||||
#include "../gameplay/Game.h"
|
||||
#include "../gameplay/LineEffect.h"
|
||||
#include <SDL3/SDL.h>
|
||||
@ -122,6 +123,10 @@ void ApplicationManager::shutdown() {
|
||||
|
||||
m_running = false;
|
||||
|
||||
// Stop audio systems before tearing down SDL to avoid aborts/asserts
|
||||
Audio::instance().shutdown();
|
||||
SoundEffectManager::instance().shutdown();
|
||||
|
||||
// Cleanup in reverse order of initialization
|
||||
cleanupManagers();
|
||||
cleanupSDL();
|
||||
@ -193,11 +198,50 @@ bool ApplicationManager::initializeManagers() {
|
||||
if (m_inputManager && m_stateManager) {
|
||||
m_inputManager->registerKeyHandler([this](SDL_Scancode sc, bool pressed){
|
||||
if (!m_stateManager) return;
|
||||
SDL_Event ev{};
|
||||
ev.type = pressed ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP;
|
||||
ev.key.scancode = sc;
|
||||
ev.key.repeat = 0;
|
||||
m_stateManager->handleEvent(ev);
|
||||
|
||||
bool consume = false;
|
||||
|
||||
// Global hotkeys (handled across all states)
|
||||
if (pressed) {
|
||||
// Toggle fullscreen on F11 or Alt+Enter (or Alt+KP_Enter)
|
||||
if (sc == SDL_SCANCODE_F11 ||
|
||||
((sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_RETURN2 || sc == SDL_SCANCODE_KP_ENTER) &&
|
||||
(SDL_GetModState() & SDL_KMOD_ALT))) {
|
||||
if (m_renderManager) {
|
||||
bool fs = m_renderManager->isFullscreen();
|
||||
m_renderManager->setFullscreen(!fs);
|
||||
}
|
||||
// Don’t also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start")
|
||||
consume = true;
|
||||
}
|
||||
|
||||
// M: Toggle/mute music; start playback if unmuting and not started yet
|
||||
if (!consume && sc == SDL_SCANCODE_M) {
|
||||
Audio::instance().toggleMute();
|
||||
m_musicEnabled = !m_musicEnabled;
|
||||
if (m_musicEnabled && !m_musicStarted && Audio::instance().getLoadedTrackCount() > 0) {
|
||||
Audio::instance().shuffle();
|
||||
Audio::instance().start();
|
||||
m_musicStarted = true;
|
||||
}
|
||||
consume = true;
|
||||
}
|
||||
|
||||
// N: Play a test sound effect
|
||||
if (!consume && sc == SDL_SCANCODE_N) {
|
||||
SoundEffectManager::instance().playSound("lets_go", 1.0f);
|
||||
consume = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to current state unless consumed
|
||||
if (!consume) {
|
||||
SDL_Event ev{};
|
||||
ev.type = pressed ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP;
|
||||
ev.key.scancode = sc;
|
||||
ev.key.repeat = 0;
|
||||
m_stateManager->handleEvent(ev);
|
||||
}
|
||||
});
|
||||
|
||||
m_inputManager->registerMouseButtonHandler([this](int button, bool pressed, float x, float y){
|
||||
@ -222,6 +266,12 @@ bool ApplicationManager::initializeManagers() {
|
||||
});
|
||||
|
||||
m_inputManager->registerWindowEventHandler([this](const SDL_WindowEvent& we){
|
||||
// Handle window resize events for RenderManager
|
||||
if (we.type == SDL_EVENT_WINDOW_RESIZED && m_renderManager) {
|
||||
m_renderManager->handleWindowResize(we.data1, we.data2);
|
||||
}
|
||||
|
||||
// Forward all window events to StateManager
|
||||
if (!m_stateManager) return;
|
||||
SDL_Event ev{};
|
||||
ev.type = SDL_EVENT_WINDOW_RESIZED; // generic mapping; handlers can inspect inner fields
|
||||
@ -230,8 +280,8 @@ bool ApplicationManager::initializeManagers() {
|
||||
});
|
||||
|
||||
m_inputManager->registerQuitHandler([this](){
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "InputManager quit handler invoked");
|
||||
SDL_Event ev{}; ev.type = SDL_EVENT_QUIT; if (m_stateManager) m_stateManager->handleEvent(ev);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[QUIT] InputManager quit handler invoked - setting running=false");
|
||||
m_running = false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -274,7 +324,9 @@ bool ApplicationManager::initializeGame() {
|
||||
// Load sound effects with fallback (SoundEffectManager already initialized)
|
||||
m_assetManager->loadSoundEffectWithFallback("clear_line", "clear_line");
|
||||
m_assetManager->loadSoundEffectWithFallback("nice_combo", "nice_combo");
|
||||
m_assetManager->loadSoundEffectWithFallback("great_move", "great_move");
|
||||
m_assetManager->loadSoundEffectWithFallback("amazing", "amazing");
|
||||
m_assetManager->loadSoundEffectWithFallback("lets_go", "lets_go");
|
||||
|
||||
// Start background music loading
|
||||
m_assetManager->startBackgroundMusicLoading();
|
||||
@ -287,6 +339,9 @@ bool ApplicationManager::initializeGame() {
|
||||
|
||||
// Create gameplay and line effect objects to populate StateContext like main.cpp
|
||||
m_lineEffect = std::make_unique<LineEffect>();
|
||||
if (m_renderManager && m_renderManager->getSDLRenderer()) {
|
||||
m_lineEffect->init(m_renderManager->getSDLRenderer());
|
||||
}
|
||||
m_game = std::make_unique<Game>(m_startLevelSelection);
|
||||
// Wire up sound callbacks as main.cpp did
|
||||
if (m_game) {
|
||||
@ -324,6 +379,12 @@ bool ApplicationManager::initializeGame() {
|
||||
}
|
||||
if (m_totalTracks > 0) {
|
||||
Audio::instance().startBackgroundLoading();
|
||||
// Kick off playback now; Audio will pick a track once decoded.
|
||||
// Do not mark as started yet; we'll flip the flag once a track is actually loaded.
|
||||
if (m_musicEnabled) {
|
||||
Audio::instance().shuffle();
|
||||
Audio::instance().start();
|
||||
}
|
||||
m_currentTrackLoading = 1; // mark started
|
||||
}
|
||||
|
||||
@ -356,6 +417,8 @@ bool ApplicationManager::initializeGame() {
|
||||
m_stateContext.backgroundTex = m_assetManager->getTexture("background");
|
||||
m_stateContext.blocksTex = m_assetManager->getTexture("blocks");
|
||||
m_stateContext.musicEnabled = &m_musicEnabled;
|
||||
m_stateContext.musicStarted = &m_musicStarted;
|
||||
m_stateContext.musicLoaded = &m_musicLoaded;
|
||||
m_stateContext.startLevelSelection = &m_startLevelSelection;
|
||||
m_stateContext.hoveredButton = &m_hoveredButton;
|
||||
m_stateContext.showSettingsPopup = &m_showSettingsPopup;
|
||||
@ -451,32 +514,22 @@ void ApplicationManager::setupStateHandlers() {
|
||||
m_starfield3D->draw(renderer.getSDLRenderer());
|
||||
}
|
||||
|
||||
// Set viewport and scaling for content
|
||||
int winW = Config::Window::DEFAULT_WIDTH;
|
||||
int winH = Config::Window::DEFAULT_HEIGHT;
|
||||
int LOGICAL_W = Config::Logical::WIDTH;
|
||||
int LOGICAL_H = Config::Logical::HEIGHT;
|
||||
|
||||
// Calculate logical scaling and viewport
|
||||
float scaleX = static_cast<float>(winW) / LOGICAL_W;
|
||||
float scaleY = static_cast<float>(winH) / LOGICAL_H;
|
||||
float logicalScale = std::min(scaleX, scaleY);
|
||||
|
||||
int vpW = static_cast<int>(LOGICAL_W * logicalScale);
|
||||
int vpH = static_cast<int>(LOGICAL_H * logicalScale);
|
||||
int vpX = (winW - vpW) / 2;
|
||||
int vpY = (winH - vpH) / 2;
|
||||
|
||||
SDL_Rect logicalVP = { vpX, vpY, vpW, vpH };
|
||||
// Set viewport and scaling for content using ACTUAL window size
|
||||
// Use RenderManager's computed logical viewport and scale so all states share the exact math
|
||||
SDL_Rect logicalVP = {0,0,0,0};
|
||||
float logicalScale = 1.0f;
|
||||
if (m_renderManager) {
|
||||
logicalVP = m_renderManager->getLogicalViewport();
|
||||
logicalScale = m_renderManager->getLogicalScale();
|
||||
}
|
||||
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
|
||||
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
|
||||
|
||||
// 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;
|
||||
// Calculate actual content area (centered within the viewport)
|
||||
// Since we already have a centered viewport, content should be drawn at (0,0) in logical space
|
||||
// The viewport itself handles the centering, so no additional offset is needed
|
||||
float contentOffsetX = 0.0f;
|
||||
float contentOffsetY = 0.0f;
|
||||
|
||||
auto drawRectOriginal = [&](float x, float y, float w, float h, SDL_Color c) {
|
||||
SDL_SetRenderDrawColor(renderer.getSDLRenderer(), c.r, c.g, c.b, c.a);
|
||||
@ -535,9 +588,9 @@ void ApplicationManager::setupStateHandlers() {
|
||||
FontAtlas* fallbackFont = (FontAtlas*)m_assetManager->getFont("main_font");
|
||||
FontAtlas* loadingFont = pixelFont ? pixelFont : fallbackFont;
|
||||
if (loadingFont) {
|
||||
const char* loadingText = "LOADING";
|
||||
float textWidth = strlen(loadingText) * 12.0f; // Approximate width for pixel font
|
||||
float textX = (LOGICAL_W - textWidth) / 2.0f;
|
||||
const std::string loadingText = "LOADING";
|
||||
int tW=0, tH=0; loadingFont->measure(loadingText, 1.0f, tW, tH);
|
||||
float textX = (LOGICAL_W - (float)tW) * 0.5f;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Rendering LOADING text at (%f,%f)", textX + contentOffsetX, currentY + contentOffsetY);
|
||||
loadingFont->draw(renderer.getSDLRenderer(), textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255, 204, 0, 255});
|
||||
} else {
|
||||
@ -568,11 +621,11 @@ void ApplicationManager::setupStateHandlers() {
|
||||
int percentage = int(loadingProgress * 100);
|
||||
char percentText[16];
|
||||
std::snprintf(percentText, sizeof(percentText), "%d%%", percentage);
|
||||
|
||||
float percentWidth = strlen(percentText) * 12.0f; // Approximate width for pixel font
|
||||
float percentX = (LOGICAL_W - percentWidth) / 2.0f;
|
||||
std::string pStr(percentText);
|
||||
int pW=0, pH=0; loadingFont->measure(pStr, 1.5f, pW, pH);
|
||||
float percentX = (LOGICAL_W - (float)pW) * 0.5f;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Rendering percent text '%s' at (%f,%f)", percentText, percentX + contentOffsetX, currentY + contentOffsetY);
|
||||
loadingFont->draw(renderer.getSDLRenderer(), percentX + contentOffsetX, currentY + contentOffsetY, percentText, 1.5f, {255, 204, 0, 255});
|
||||
loadingFont->draw(renderer.getSDLRenderer(), percentX + contentOffsetX, currentY + contentOffsetY, pStr, 1.5f, {255, 204, 0, 255});
|
||||
}
|
||||
|
||||
// Reset viewport and scale
|
||||
@ -584,12 +637,18 @@ void ApplicationManager::setupStateHandlers() {
|
||||
[this](float deltaTime) {
|
||||
// Update 3D starfield so stars move during loading
|
||||
if (m_starfield3D) {
|
||||
m_starfield3D->update(deltaTime);
|
||||
// deltaTime here is in milliseconds; Starfield3D expects seconds
|
||||
m_starfield3D->update(deltaTime / 1000.0f);
|
||||
}
|
||||
|
||||
// Check if loading is complete and transition to menu
|
||||
if (m_assetManager->isLoadingComplete()) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading complete, transitioning to Menu");
|
||||
|
||||
// Update texture pointers now that assets are loaded
|
||||
m_stateContext.backgroundTex = m_assetManager->getTexture("background");
|
||||
m_stateContext.blocksTex = m_assetManager->getTexture("blocks");
|
||||
|
||||
bool ok = m_stateManager->setState(AppState::Menu);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "setState(AppState::Menu) returned %d", ok ? 1 : 0);
|
||||
traceFile("- to Menu returned");
|
||||
@ -609,19 +668,13 @@ void ApplicationManager::setupStateHandlers() {
|
||||
renderer.renderTexture(background, nullptr, &bgRect);
|
||||
}
|
||||
|
||||
// Compute logical scale and viewport
|
||||
const int LOGICAL_W = Config::Logical::WIDTH;
|
||||
const int LOGICAL_H = Config::Logical::HEIGHT;
|
||||
float scaleX = winW > 0 ? (float)winW / LOGICAL_W : 1.0f;
|
||||
float scaleY = winH > 0 ? (float)winH / LOGICAL_H : 1.0f;
|
||||
float logicalScale = std::min(scaleX, scaleY);
|
||||
int vpW = (int)(LOGICAL_W * logicalScale);
|
||||
int vpH = (int)(LOGICAL_H * logicalScale);
|
||||
int vpX = (winW - vpW) / 2;
|
||||
int vpY = (winH - vpH) / 2;
|
||||
SDL_Rect logicalVP{vpX, vpY, vpW, vpH};
|
||||
|
||||
// Apply viewport+scale then call MenuState::render (shows highscores, fireworks, bottom buttons)
|
||||
// Use RenderManager's computed logical viewport/scale for exact centering
|
||||
SDL_Rect logicalVP = {0,0,0,0};
|
||||
float logicalScale = 1.0f;
|
||||
if (m_renderManager) {
|
||||
logicalVP = m_renderManager->getLogicalViewport();
|
||||
logicalScale = m_renderManager->getLogicalScale();
|
||||
}
|
||||
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
|
||||
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
|
||||
if (m_menuState) {
|
||||
@ -645,19 +698,13 @@ void ApplicationManager::setupStateHandlers() {
|
||||
renderer.renderTexture(background, nullptr, &bgRect);
|
||||
}
|
||||
|
||||
// Compute logical scale and viewport
|
||||
const int LOGICAL_W = Config::Logical::WIDTH;
|
||||
const int LOGICAL_H = Config::Logical::HEIGHT;
|
||||
float scaleX = winW > 0 ? (float)winW / LOGICAL_W : 1.0f;
|
||||
float scaleY = winH > 0 ? (float)winH / LOGICAL_H : 1.0f;
|
||||
float logicalScale = std::min(scaleX, scaleY);
|
||||
int vpW = (int)(LOGICAL_W * logicalScale);
|
||||
int vpH = (int)(LOGICAL_H * logicalScale);
|
||||
int vpX = (winW - vpW) / 2;
|
||||
int vpY = (winH - vpH) / 2;
|
||||
SDL_Rect logicalVP{vpX, vpY, vpW, vpH};
|
||||
|
||||
// Apply viewport+scale then call LevelSelectorState::render (shows level selection popup)
|
||||
// Use RenderManager's computed logical viewport/scale for exact centering
|
||||
SDL_Rect logicalVP = {0,0,0,0};
|
||||
float logicalScale = 1.0f;
|
||||
if (m_renderManager) {
|
||||
logicalVP = m_renderManager->getLogicalViewport();
|
||||
logicalScale = m_renderManager->getLogicalScale();
|
||||
}
|
||||
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
|
||||
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
|
||||
if (m_levelSelectorState) {
|
||||
@ -671,13 +718,17 @@ void ApplicationManager::setupStateHandlers() {
|
||||
m_stateManager->registerUpdateHandler(AppState::Menu,
|
||||
[this](float deltaTime) {
|
||||
// Update logo animation counter
|
||||
m_logoAnimCounter += deltaTime;
|
||||
// deltaTime is in milliseconds; keep same behavior as main.cpp: counter += frameMs * 0.0008
|
||||
m_logoAnimCounter += (deltaTime * static_cast<float>(Config::Animation::LOGO_ANIM_SPEED));
|
||||
// Also keep GlobalState's counter in sync for UI effects that read from it
|
||||
GlobalState::instance().logoAnimCounter += (deltaTime * Config::Animation::LOGO_ANIM_SPEED);
|
||||
|
||||
// Update fireworks effect
|
||||
GlobalState& globalState = GlobalState::instance();
|
||||
// updateFireworks expects milliseconds
|
||||
globalState.updateFireworks(deltaTime);
|
||||
|
||||
// Start background music once tracks are available and not yet started
|
||||
// Start music as soon as at least one track has decoded (don’t wait for all)
|
||||
if (m_musicEnabled && !m_musicStarted) {
|
||||
if (Audio::instance().getLoadedTrackCount() > 0) {
|
||||
Audio::instance().shuffle();
|
||||
@ -685,6 +736,10 @@ void ApplicationManager::setupStateHandlers() {
|
||||
m_musicStarted = true;
|
||||
}
|
||||
}
|
||||
// Track completion status for UI
|
||||
if (!m_musicLoaded && Audio::instance().isLoadingComplete()) {
|
||||
m_musicLoaded = true;
|
||||
}
|
||||
});
|
||||
|
||||
m_stateManager->registerEventHandler(AppState::Menu,
|
||||
@ -711,17 +766,10 @@ void ApplicationManager::setupStateHandlers() {
|
||||
m_showExitConfirmPopup = true;
|
||||
return;
|
||||
}
|
||||
// Global toggles
|
||||
if (event.key.scancode == SDL_SCANCODE_M) {
|
||||
Audio::instance().toggleMute();
|
||||
m_musicEnabled = !m_musicEnabled;
|
||||
}
|
||||
// S: toggle SFX enable state (music handled globally)
|
||||
if (event.key.scancode == SDL_SCANCODE_S) {
|
||||
SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled());
|
||||
}
|
||||
if (event.key.scancode == SDL_SCANCODE_N) {
|
||||
SoundEffectManager::instance().playSound("lets_go", 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse handling: map SDL mouse coords into logical content coords and
|
||||
@ -729,11 +777,9 @@ void ApplicationManager::setupStateHandlers() {
|
||||
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
float mx = (float)event.button.x;
|
||||
float my = (float)event.button.y;
|
||||
int winW = 0, winH = 0;
|
||||
if (m_renderManager) m_renderManager->getWindowSize(winW, winH);
|
||||
float logicalScale = std::min(winW / (float)Config::Logical::WIDTH, winH / (float)Config::Logical::HEIGHT);
|
||||
if (logicalScale <= 0) logicalScale = 1.0f;
|
||||
SDL_Rect logicalVP{0,0,winW,winH};
|
||||
// Use RenderManager's computed logical viewport/scale for precise mapping
|
||||
SDL_Rect logicalVP{0,0,0,0}; float logicalScale = 1.0f;
|
||||
if (m_renderManager) { logicalVP = m_renderManager->getLogicalViewport(); logicalScale = m_renderManager->getLogicalScale(); }
|
||||
// Check bounds and compute content-local coords
|
||||
if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h) {
|
||||
float lx = (mx - logicalVP.x) / logicalScale;
|
||||
@ -773,11 +819,9 @@ void ApplicationManager::setupStateHandlers() {
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
float mx = (float)event.motion.x;
|
||||
float my = (float)event.motion.y;
|
||||
int winW = 0, winH = 0;
|
||||
if (m_renderManager) m_renderManager->getWindowSize(winW, winH);
|
||||
float logicalScale = std::min(winW / (float)Config::Logical::WIDTH, winH / (float)Config::Logical::HEIGHT);
|
||||
if (logicalScale <= 0) logicalScale = 1.0f;
|
||||
SDL_Rect logicalVP{0,0,winW,winH};
|
||||
// Use RenderManager's computed logical viewport/scale for precise mapping
|
||||
SDL_Rect logicalVP{0,0,0,0}; float logicalScale = 1.0f;
|
||||
if (m_renderManager) { logicalVP = m_renderManager->getLogicalViewport(); logicalScale = m_renderManager->getLogicalScale(); }
|
||||
if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h) {
|
||||
float lx = (mx - logicalVP.x) / logicalScale;
|
||||
float ly = (my - logicalVP.y) / logicalScale;
|
||||
@ -801,35 +845,301 @@ void ApplicationManager::setupStateHandlers() {
|
||||
}
|
||||
});
|
||||
|
||||
// Playing State - Placeholder for now
|
||||
m_stateManager->registerRenderHandler(AppState::Playing,
|
||||
[this](RenderManager& renderer) {
|
||||
renderer.clear(0, 0, 0, 255);
|
||||
|
||||
// For now, just show a placeholder
|
||||
FontAtlas* font = (FontAtlas*)m_assetManager->getFont("main_font");
|
||||
if (font) {
|
||||
float centerX = Config::Window::DEFAULT_WIDTH / 2.0f;
|
||||
float centerY = Config::Window::DEFAULT_HEIGHT / 2.0f;
|
||||
std::string playingText = "TETRIS GAME PLAYING STATE";
|
||||
float textX = centerX - (playingText.length() * 12.0f) / 2.0f;
|
||||
font->draw(renderer.getSDLRenderer(), textX, centerY, playingText, 2.0f, {255, 255, 255, 255});
|
||||
|
||||
std::string instruction = "Press ESC to return to menu";
|
||||
float instrX = centerX - (instruction.length() * 8.0f) / 2.0f;
|
||||
font->draw(renderer.getSDLRenderer(), instrX, centerY + 60, instruction, 1.0f, {200, 200, 200, 255});
|
||||
// GameOver State - Handle restart and return to menu
|
||||
m_stateManager->registerEventHandler(AppState::GameOver,
|
||||
[this](const SDL_Event& event) {
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||
// Enter/Space - restart game
|
||||
if (event.key.scancode == SDL_SCANCODE_RETURN ||
|
||||
event.key.scancode == SDL_SCANCODE_RETURN2 ||
|
||||
event.key.scancode == SDL_SCANCODE_KP_ENTER ||
|
||||
event.key.scancode == SDL_SCANCODE_SPACE) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Restarting game from GameOver (Enter/Space)");
|
||||
// Reset game with current start level and transition to Playing
|
||||
if (m_stateContext.game) {
|
||||
m_stateContext.game->reset(m_startLevelSelection);
|
||||
}
|
||||
m_stateManager->setState(AppState::Playing);
|
||||
return;
|
||||
}
|
||||
// Escape - return to menu
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Returning to menu from GameOver (Escape)");
|
||||
m_stateManager->setState(AppState::Menu);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Playing State - Full game rendering
|
||||
m_stateManager->registerEventHandler(AppState::Playing,
|
||||
[this](const SDL_Event& event) {
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Returning to menu from playing state");
|
||||
m_stateManager->setState(AppState::Menu);
|
||||
// Handle mouse clicks on the exit confirmation popup
|
||||
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && m_showExitConfirmPopup) {
|
||||
float mx = (float)event.button.x;
|
||||
float my = (float)event.button.y;
|
||||
|
||||
int winW = 0, winH = 0;
|
||||
if (m_renderManager) m_renderManager->getWindowSize(winW, winH);
|
||||
|
||||
const float LOGICAL_W = static_cast<float>(Config::Logical::WIDTH);
|
||||
const float LOGICAL_H = static_cast<float>(Config::Logical::HEIGHT);
|
||||
float scaleX = (winW > 0) ? (float)winW / LOGICAL_W : 1.0f;
|
||||
float scaleY = (winH > 0) ? (float)winH / LOGICAL_H : 1.0f;
|
||||
float logicalScale = std::min(scaleX, scaleY);
|
||||
|
||||
SDL_Rect logicalVP{0, 0, winW, winH};
|
||||
if (mx < logicalVP.x || my < logicalVP.y || mx > logicalVP.x + logicalVP.w || my > logicalVP.y + logicalVP.h) return;
|
||||
|
||||
float lx = (mx - logicalVP.x) / (logicalScale > 0.f ? logicalScale : 1.f);
|
||||
float ly = (my - logicalVP.y) / (logicalScale > 0.f ? logicalScale : 1.f);
|
||||
|
||||
// Compute content offsets to convert to content-local logical coords (what renderer uses)
|
||||
float contentW = LOGICAL_W * logicalScale;
|
||||
float contentH = LOGICAL_H * logicalScale;
|
||||
float contentOffsetX = (winW - contentW) * 0.5f / (logicalScale > 0.f ? logicalScale : 1.f);
|
||||
float contentOffsetY = (winH - contentH) * 0.5f / (logicalScale > 0.f ? logicalScale : 1.f);
|
||||
float localX = lx - contentOffsetX;
|
||||
float localY = ly - contentOffsetY;
|
||||
|
||||
// Popup geometry (must match GameRenderer)
|
||||
float popupW = 420.0f, popupH = 180.0f;
|
||||
float popupX = (LOGICAL_W - popupW) * 0.5f;
|
||||
float popupY = (LOGICAL_H - popupH) * 0.5f;
|
||||
float btnW = 140.0f, btnH = 46.0f;
|
||||
float yesX = popupX + popupW * 0.25f - btnW * 0.5f;
|
||||
float noX = popupX + popupW * 0.75f - btnW * 0.5f;
|
||||
float btnY = popupY + popupH - 60.0f;
|
||||
|
||||
// Only react if click is inside popup
|
||||
if (localX >= popupX && localX <= popupX + popupW && localY >= popupY && localY <= popupY + popupH) {
|
||||
if (localX >= yesX && localX <= yesX + btnW && localY >= btnY && localY <= btnY + btnH) {
|
||||
// YES: go back to menu (reset game)
|
||||
m_showExitConfirmPopup = false;
|
||||
if (m_stateContext.game) m_stateContext.game->reset(m_startLevelSelection);
|
||||
if (m_stateManager) m_stateManager->setState(AppState::Menu);
|
||||
return;
|
||||
}
|
||||
if (localX >= noX && localX <= noX + btnW && localY >= btnY && localY <= btnY + btnH) {
|
||||
// NO: close popup and resume
|
||||
m_showExitConfirmPopup = false;
|
||||
if (m_stateContext.game) m_stateContext.game->setPaused(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
m_stateManager->registerRenderHandler(AppState::Playing,
|
||||
[this](RenderManager& renderer) {
|
||||
// Clear the screen first
|
||||
renderer.clear(0, 0, 0, 255);
|
||||
|
||||
// Window size
|
||||
int winW = 0, winH = 0;
|
||||
renderer.getWindowSize(winW, winH);
|
||||
|
||||
// Draw per-level background stretched to full window, with fade
|
||||
if (m_stateContext.game) {
|
||||
// Update fade progression (ms based on frame time not available here; approximate using SDL ticks delta if desired)
|
||||
// We'll keep alpha as-is; Loading/Menu update can adjust if we wire a timer. For now, simply show the correct background.
|
||||
int currentLevel = m_stateContext.game->level();
|
||||
int bgLevel = (currentLevel > 32) ? 32 : currentLevel; // Cap at 32 like main.cpp
|
||||
|
||||
if (m_cachedBgLevel != bgLevel) {
|
||||
if (m_nextLevelBackgroundTex) { SDL_DestroyTexture(m_nextLevelBackgroundTex); m_nextLevelBackgroundTex = nullptr; }
|
||||
char bgPath[256];
|
||||
std::snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.bmp", bgLevel);
|
||||
SDL_Surface* s = SDL_LoadBMP(bgPath);
|
||||
if (s && renderer.getSDLRenderer()) {
|
||||
m_nextLevelBackgroundTex = SDL_CreateTextureFromSurface(renderer.getSDLRenderer(), s);
|
||||
SDL_DestroySurface(s);
|
||||
m_levelFadeAlpha = 0.0f;
|
||||
m_levelFadeElapsed = 0.0f;
|
||||
m_cachedBgLevel = bgLevel;
|
||||
} else {
|
||||
m_cachedBgLevel = -1; // don’t change if missing
|
||||
if (s) SDL_DestroySurface(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (winW > 0 && winH > 0) {
|
||||
SDL_FRect full{0,0,(float)winW,(float)winH};
|
||||
if (m_nextLevelBackgroundTex && m_levelFadeAlpha < 1.0f && m_levelBackgroundTex) {
|
||||
SDL_SetTextureAlphaMod(m_levelBackgroundTex, Uint8((1.0f - m_levelFadeAlpha) * 255));
|
||||
SDL_RenderTexture(renderer.getSDLRenderer(), m_levelBackgroundTex, nullptr, &full);
|
||||
SDL_SetTextureAlphaMod(m_nextLevelBackgroundTex, Uint8(m_levelFadeAlpha * 255));
|
||||
SDL_RenderTexture(renderer.getSDLRenderer(), m_nextLevelBackgroundTex, nullptr, &full);
|
||||
SDL_SetTextureAlphaMod(m_levelBackgroundTex, 255);
|
||||
SDL_SetTextureAlphaMod(m_nextLevelBackgroundTex, 255);
|
||||
} else if (m_nextLevelBackgroundTex && (!m_levelBackgroundTex || m_levelFadeAlpha >= 1.0f)) {
|
||||
if (m_levelBackgroundTex) SDL_DestroyTexture(m_levelBackgroundTex);
|
||||
m_levelBackgroundTex = m_nextLevelBackgroundTex;
|
||||
m_nextLevelBackgroundTex = nullptr;
|
||||
m_levelFadeAlpha = 0.0f;
|
||||
SDL_RenderTexture(renderer.getSDLRenderer(), m_levelBackgroundTex, nullptr, &full);
|
||||
} else if (m_levelBackgroundTex) {
|
||||
SDL_RenderTexture(renderer.getSDLRenderer(), m_levelBackgroundTex, nullptr, &full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute logical scale from logical design size
|
||||
const float LOGICAL_W = static_cast<float>(Config::Logical::WIDTH);
|
||||
const float LOGICAL_H = static_cast<float>(Config::Logical::HEIGHT);
|
||||
float scaleX = (winW > 0) ? (float)winW / LOGICAL_W : 1.0f;
|
||||
float scaleY = (winH > 0) ? (float)winH / LOGICAL_H : 1.0f;
|
||||
float logicalScale = std::min(scaleX, scaleY);
|
||||
|
||||
// Use full-window viewport; GameRenderer applies its own content offsets for centering
|
||||
SDL_Rect logicalVP = {0, 0, winW, winH};
|
||||
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
|
||||
|
||||
// Use GameRenderer for actual game rendering
|
||||
GameRenderer::renderPlayingState(
|
||||
renderer.getSDLRenderer(),
|
||||
m_stateContext.game,
|
||||
m_stateContext.pixelFont,
|
||||
m_stateContext.lineEffect,
|
||||
m_stateContext.blocksTex,
|
||||
LOGICAL_W,
|
||||
LOGICAL_H,
|
||||
logicalScale,
|
||||
static_cast<float>(winW),
|
||||
static_cast<float>(winH),
|
||||
m_showExitConfirmPopup
|
||||
);
|
||||
|
||||
// Reset viewport
|
||||
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
|
||||
});
|
||||
|
||||
// GameOver State - Simple game over screen
|
||||
m_stateManager->registerRenderHandler(AppState::GameOver,
|
||||
[this](RenderManager& renderer) {
|
||||
// Clear the screen first
|
||||
renderer.clear(12, 12, 16, 255);
|
||||
|
||||
// Calculate viewport and scale for responsive layout
|
||||
int winW = 0, winH = 0;
|
||||
renderer.getWindowSize(winW, winH);
|
||||
|
||||
const float LOGICAL_W = static_cast<float>(Config::Window::DEFAULT_WIDTH);
|
||||
const float LOGICAL_H = static_cast<float>(Config::Window::DEFAULT_HEIGHT);
|
||||
|
||||
float scaleX = static_cast<float>(winW) / LOGICAL_W;
|
||||
float scaleY = static_cast<float>(winH) / LOGICAL_H;
|
||||
float logicalScale = std::min(scaleX, scaleY);
|
||||
|
||||
int scaledW = static_cast<int>(LOGICAL_W * logicalScale);
|
||||
int scaledH = static_cast<int>(LOGICAL_H * logicalScale);
|
||||
int offsetX = (winW - scaledW) / 2;
|
||||
int offsetY = (winH - scaledH) / 2;
|
||||
|
||||
SDL_Rect logicalVP = {offsetX, offsetY, scaledW, scaledH};
|
||||
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
|
||||
|
||||
// Draw starfield background
|
||||
if (m_starfield) {
|
||||
m_starfield->draw(renderer.getSDLRenderer());
|
||||
}
|
||||
|
||||
// Game over text and stats
|
||||
if (m_stateContext.pixelFont && m_stateContext.game) {
|
||||
FontAtlas& font = *m_stateContext.pixelFont;
|
||||
|
||||
// "GAME OVER" title
|
||||
font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 120, 140, "GAME OVER", 3.0f, {255, 80, 60, 255});
|
||||
|
||||
// Game stats
|
||||
char buf[128];
|
||||
std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d",
|
||||
m_stateContext.game->score(),
|
||||
m_stateContext.game->lines(),
|
||||
m_stateContext.game->level());
|
||||
font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 180, 220, buf, 1.2f, {220, 220, 230, 255});
|
||||
|
||||
// Instructions
|
||||
font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 120, 270, "PRESS ENTER / SPACE", 1.2f, {200, 200, 220, 255});
|
||||
}
|
||||
|
||||
// Reset viewport
|
||||
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
|
||||
});
|
||||
|
||||
// Playing State - Update handler for DAS/ARR movement timing
|
||||
m_stateManager->registerUpdateHandler(AppState::Playing,
|
||||
[this](double frameMs) {
|
||||
if (!m_stateContext.game) return;
|
||||
|
||||
// Get current keyboard state
|
||||
const bool *ks = SDL_GetKeyboardState(nullptr);
|
||||
bool left = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_A];
|
||||
bool right = ks[SDL_SCANCODE_RIGHT] || ks[SDL_SCANCODE_D];
|
||||
bool down = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_S];
|
||||
|
||||
// Handle soft drop
|
||||
m_stateContext.game->setSoftDropping(down && !m_stateContext.game->isPaused());
|
||||
|
||||
// Handle DAS/ARR movement timing (from original main.cpp)
|
||||
int moveDir = 0;
|
||||
if (left && !right)
|
||||
moveDir = -1;
|
||||
else if (right && !left)
|
||||
moveDir = +1;
|
||||
|
||||
if (moveDir != 0 && !m_stateContext.game->isPaused()) {
|
||||
if ((moveDir == -1 && !m_leftHeld) || (moveDir == +1 && !m_rightHeld)) {
|
||||
// First press - immediate movement
|
||||
m_stateContext.game->move(moveDir);
|
||||
m_moveTimerMs = DAS; // Set initial delay
|
||||
} else {
|
||||
// Key held - handle repeat timing
|
||||
m_moveTimerMs -= frameMs;
|
||||
if (m_moveTimerMs <= 0) {
|
||||
m_stateContext.game->move(moveDir);
|
||||
m_moveTimerMs += ARR; // Set repeat rate
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m_moveTimerMs = 0; // Reset timer when no movement
|
||||
}
|
||||
|
||||
// Update held state for next frame
|
||||
m_leftHeld = left;
|
||||
m_rightHeld = right;
|
||||
|
||||
// Handle soft drop boost
|
||||
if (down && !m_stateContext.game->isPaused()) {
|
||||
m_stateContext.game->softDropBoost(frameMs);
|
||||
}
|
||||
|
||||
// Delegate to PlayingState for other updates (gravity, line effects)
|
||||
if (m_playingState) {
|
||||
m_playingState->update(frameMs);
|
||||
}
|
||||
|
||||
// Update background fade progression (match main.cpp semantics approx)
|
||||
// Duration 1200ms fade (same as LEVEL_FADE_DURATION used in main.cpp snippets)
|
||||
const float LEVEL_FADE_DURATION = 1200.0f;
|
||||
if (m_nextLevelBackgroundTex) {
|
||||
m_levelFadeElapsed += (float)frameMs;
|
||||
m_levelFadeAlpha = std::min(1.0f, m_levelFadeElapsed / LEVEL_FADE_DURATION);
|
||||
}
|
||||
|
||||
// Check for game over and transition to GameOver state
|
||||
if (m_stateContext.game->isGameOver()) {
|
||||
// Submit score before transitioning
|
||||
if (m_stateContext.scores) {
|
||||
m_stateContext.scores->submit(
|
||||
m_stateContext.game->score(),
|
||||
m_stateContext.game->lines(),
|
||||
m_stateContext.game->level(),
|
||||
m_stateContext.game->elapsed()
|
||||
);
|
||||
}
|
||||
m_stateManager->setState(AppState::GameOver);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ApplicationManager::processEvents() {
|
||||
@ -867,7 +1177,9 @@ void ApplicationManager::update(float deltaTime) {
|
||||
|
||||
// Update StateManager
|
||||
if (m_stateManager) {
|
||||
m_stateManager->update(deltaTime);
|
||||
// NOTE: State update handlers expect milliseconds (frameMs). Convert seconds -> ms here.
|
||||
float frameMs = deltaTime * 1000.0f;
|
||||
m_stateManager->update(frameMs);
|
||||
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::update - state update completed for state %s", m_stateManager->getStateName(m_stateManager->getState()));
|
||||
traceFile("update completed");
|
||||
}
|
||||
@ -896,6 +1208,12 @@ void ApplicationManager::render() {
|
||||
|
||||
void ApplicationManager::cleanupManagers() {
|
||||
// Cleanup managers in reverse order
|
||||
// Destroy gameplay background textures
|
||||
if (m_levelBackgroundTex) { SDL_DestroyTexture(m_levelBackgroundTex); m_levelBackgroundTex = nullptr; }
|
||||
if (m_nextLevelBackgroundTex) { SDL_DestroyTexture(m_nextLevelBackgroundTex); m_nextLevelBackgroundTex = nullptr; }
|
||||
// Shutdown subsystems that own GPU resources before renderer destruction
|
||||
if (m_lineEffect) { m_lineEffect->shutdown(); }
|
||||
// Fonts are managed by AssetManager; ensure it shuts down after we stop states
|
||||
m_stateManager.reset();
|
||||
m_assetManager.reset();
|
||||
m_inputManager.reset();
|
||||
|
||||
@ -96,6 +96,12 @@ private:
|
||||
std::unique_ptr<Game> m_game;
|
||||
std::unique_ptr<LineEffect> m_lineEffect;
|
||||
|
||||
// DAS/ARR movement timing (from original main.cpp)
|
||||
bool m_leftHeld = false;
|
||||
bool m_rightHeld = false;
|
||||
double m_moveTimerMs = 0.0;
|
||||
static constexpr double DAS = 170.0; // Delayed Auto Shift
|
||||
static constexpr double ARR = 40.0; // Auto Repeat Rate
|
||||
|
||||
// State context (must be a member to ensure lifetime)
|
||||
StateContext m_stateContext;
|
||||
@ -119,4 +125,11 @@ private:
|
||||
|
||||
// Animation state
|
||||
float m_logoAnimCounter = 0.0f;
|
||||
|
||||
// Gameplay background (per-level) with fade, mirroring main.cpp behavior
|
||||
SDL_Texture* m_levelBackgroundTex = nullptr;
|
||||
SDL_Texture* m_nextLevelBackgroundTex = nullptr; // used during fade transitions
|
||||
float m_levelFadeAlpha = 0.0f; // 0..1 blend factor
|
||||
float m_levelFadeElapsed = 0.0f; // ms
|
||||
int m_cachedBgLevel = -1; // last loaded background level index
|
||||
};
|
||||
|
||||
@ -46,8 +46,9 @@ void GlobalState::updateFireworks(double frameMs) {
|
||||
|
||||
// Create new fireworks occasionally
|
||||
if (currentTime - lastFireworkTime > 800 + (rand() % 1200)) {
|
||||
float x = Config::Logical::WIDTH * 0.2f + (rand() % (int)(Config::Logical::WIDTH * 0.6f));
|
||||
float y = Config::Logical::HEIGHT * 0.3f + (rand() % (int)(Config::Logical::HEIGHT * 0.4f));
|
||||
// Spawn bias similar to legacy: lower-right area
|
||||
float x = Config::Logical::WIDTH * (0.55f + (rand() % 35) / 100.0f); // ~55% - 90%
|
||||
float y = Config::Logical::HEIGHT * (0.80f + (rand() % 15) / 100.0f); // ~80% - 95%
|
||||
createFirework(x, y);
|
||||
lastFireworkTime = currentTime;
|
||||
}
|
||||
@ -60,15 +61,17 @@ void GlobalState::updateFireworks(double frameMs) {
|
||||
for (auto& particle : firework.particles) {
|
||||
if (particle.life <= 0) continue;
|
||||
|
||||
// Update physics
|
||||
particle.x += particle.vx * (frameMs / 1000.0f);
|
||||
particle.y += particle.vy * (frameMs / 1000.0f);
|
||||
particle.vy += 150.0f * (frameMs / 1000.0f); // Gravity
|
||||
// Update physics (gentler gravity, slight friction)
|
||||
float dt = float(frameMs / 1000.0f);
|
||||
particle.x += particle.vx * dt;
|
||||
particle.y += particle.vy * dt;
|
||||
particle.vx *= (1.0f - 0.6f * dt); // horizontal friction
|
||||
particle.vy = particle.vy * (1.0f - 0.3f * dt) + 90.0f * dt; // gravity with damping
|
||||
particle.life -= frameMs;
|
||||
|
||||
// Fade size over time
|
||||
// Smaller particles overall
|
||||
float lifeRatio = particle.life / particle.maxLife;
|
||||
particle.size = 20.0f + 10.0f * lifeRatio;
|
||||
particle.size = 6.0f + 5.0f * lifeRatio;
|
||||
|
||||
if (particle.life > 0) {
|
||||
hasActiveParticles = true;
|
||||
@ -100,7 +103,7 @@ void GlobalState::createFirework(float x, float y) {
|
||||
firework->particles.clear();
|
||||
|
||||
// Create particles
|
||||
const int particleCount = 12 + (rand() % 8);
|
||||
const int particleCount = 10 + (rand() % 6);
|
||||
for (int i = 0; i < particleCount; ++i) {
|
||||
BlockParticle particle;
|
||||
particle.x = x;
|
||||
@ -108,14 +111,14 @@ void GlobalState::createFirework(float x, float y) {
|
||||
|
||||
// Random velocity in all directions
|
||||
float angle = (float)(rand() % 360) * 3.14159f / 180.0f;
|
||||
float speed = 80.0f + (rand() % 120);
|
||||
float speed = 70.0f + (rand() % 90);
|
||||
particle.vx = cos(angle) * speed;
|
||||
particle.vy = sin(angle) * speed - 50.0f; // Slight upward bias
|
||||
|
||||
particle.type = 1 + (rand() % 7); // Random tetris piece color
|
||||
particle.maxLife = 1500.0f + (rand() % 1000); // 1.5-2.5 seconds
|
||||
particle.maxLife = 1200.0f + (rand() % 800); // ~1.2-2.0 seconds
|
||||
particle.life = particle.maxLife;
|
||||
particle.size = 15.0f + (rand() % 15);
|
||||
particle.size = 6.0f + (rand() % 5);
|
||||
|
||||
firework->particles.push_back(particle);
|
||||
}
|
||||
@ -132,7 +135,8 @@ void GlobalState::drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex)
|
||||
|
||||
// Calculate alpha based on remaining life
|
||||
float lifeRatio = particle.life / particle.maxLife;
|
||||
Uint8 alpha = (Uint8)(255 * std::min(1.0f, lifeRatio * 2.0f));
|
||||
// Faster fade like legacy
|
||||
Uint8 alpha = (Uint8)(255 * std::min(1.0f, lifeRatio * 1.6f));
|
||||
|
||||
// Set texture alpha
|
||||
SDL_SetTextureAlphaMod(blocksTex, alpha);
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#include "gameplay/Game.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
// Piece rotation bitmasks (row-major 4x4). Bit 0 = (0,0).
|
||||
static const std::array<Shape, PIECE_COUNT> SHAPES = {{
|
||||
@ -238,7 +239,9 @@ bool Game::tryMoveDown() {
|
||||
|
||||
void Game::tickGravity(double frameMs) {
|
||||
if (paused) return; // Don't tick gravity when paused
|
||||
|
||||
fallAcc += frameMs;
|
||||
|
||||
while (fallAcc >= gravityMs) {
|
||||
// Attempt to move down by one row
|
||||
if (tryMoveDown()) {
|
||||
|
||||
@ -98,7 +98,7 @@ private:
|
||||
LevelUpCallback levelUpCallback;
|
||||
// Gravity tuning -----------------------------------------------------
|
||||
// Global multiplier applied to all level timings (use to slow/speed whole-game gravity)
|
||||
double gravityGlobalMultiplier{2.8};
|
||||
double gravityGlobalMultiplier{1.0};
|
||||
// Gravity manager encapsulates frames table, multipliers and conversions
|
||||
GravityManager gravityMgr;
|
||||
// Backwards-compatible accessors (delegate to gravityMgr)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#include "LineEffect.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include "audio/Audio.h"
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
@ -80,15 +81,11 @@ bool LineEffect::init(SDL_Renderer* r) {
|
||||
}
|
||||
|
||||
void LineEffect::shutdown() {
|
||||
if (audioStream) {
|
||||
SDL_DestroyAudioStream(audioStream);
|
||||
audioStream = nullptr;
|
||||
}
|
||||
// No separate audio stream anymore; SFX go through shared Audio mixer
|
||||
}
|
||||
|
||||
void LineEffect::initAudio() {
|
||||
// For now, we'll generate simple beep sounds procedurally
|
||||
// In a full implementation, you'd load WAV files
|
||||
// Generate simple beep sounds procedurally (fallback when voice SFX not provided)
|
||||
|
||||
// Generate a simple line clear beep (440Hz for 0.2 seconds)
|
||||
int sampleRate = 44100;
|
||||
@ -265,33 +262,10 @@ void LineEffect::renderExplosion() {
|
||||
}
|
||||
|
||||
void LineEffect::playLineClearSound(int lineCount) {
|
||||
if (!audioStream) {
|
||||
// Create audio stream for sound effects
|
||||
SDL_AudioSpec spec = {};
|
||||
spec.format = SDL_AUDIO_S16;
|
||||
spec.channels = 2;
|
||||
spec.freq = 44100;
|
||||
|
||||
audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
|
||||
if (!audioStream) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Could not create audio stream for line clear effects");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Choose appropriate sound based on line count
|
||||
const std::vector<int16_t>* sample = nullptr;
|
||||
|
||||
if (lineCount == 4) {
|
||||
sample = &tetrisSample; // Special sound for Tetris
|
||||
//printf("TETRIS! 4 lines cleared!\n");
|
||||
} else {
|
||||
sample = &lineClearSample; // Regular line clear sound
|
||||
//printf("Line clear: %d lines\n", lineCount);
|
||||
}
|
||||
|
||||
const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample;
|
||||
if (sample && !sample->empty()) {
|
||||
SDL_PutAudioStreamData(audioStream, sample->data(),
|
||||
static_cast<int>(sample->size() * sizeof(int16_t)));
|
||||
// Mix via shared Audio device so it layers with music
|
||||
Audio::instance().playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f);
|
||||
}
|
||||
}
|
||||
|
||||
480
src/graphics/GameRenderer.cpp
Normal file
480
src/graphics/GameRenderer.cpp
Normal file
@ -0,0 +1,480 @@
|
||||
#include "GameRenderer.h"
|
||||
#include "../gameplay/Game.h"
|
||||
#include "../graphics/Font.h"
|
||||
#include "../gameplay/LineEffect.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
// Color constants (copied from main.cpp)
|
||||
static const SDL_Color COLORS[] = {
|
||||
{0, 0, 0, 255}, // 0: BLACK (empty)
|
||||
{0, 255, 255, 255}, // 1: I-piece - cyan
|
||||
{255, 255, 0, 255}, // 2: O-piece - yellow
|
||||
{128, 0, 128, 255}, // 3: T-piece - purple
|
||||
{0, 255, 0, 255}, // 4: S-piece - green
|
||||
{255, 0, 0, 255}, // 5: Z-piece - red
|
||||
{0, 0, 255, 255}, // 6: J-piece - blue
|
||||
{255, 165, 0, 255} // 7: L-piece - orange
|
||||
};
|
||||
|
||||
void GameRenderer::drawRect(SDL_Renderer* renderer, 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, y, w, h};
|
||||
SDL_RenderFillRect(renderer, &fr);
|
||||
}
|
||||
|
||||
void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) {
|
||||
if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) {
|
||||
// Fallback to colored rectangle if texture isn't available
|
||||
SDL_Color color = (blockType >= 0 && blockType < PIECE_COUNT) ? COLORS[blockType + 1] : SDL_Color{128, 128, 128, 255};
|
||||
drawRect(renderer, x, y, size-1, size-1, color);
|
||||
return;
|
||||
}
|
||||
|
||||
// JavaScript uses: sx = type * spriteSize, sy = 0, with 2px padding
|
||||
// Each sprite is 90px wide in the horizontal sprite sheet
|
||||
const int SPRITE_SIZE = 90;
|
||||
float srcX = blockType * SPRITE_SIZE + 2; // Add 2px padding like JS
|
||||
float srcY = 2; // Add 2px padding from top like JS
|
||||
float srcW = SPRITE_SIZE - 4; // Subtract 4px total padding like JS
|
||||
float srcH = SPRITE_SIZE - 4; // Subtract 4px total padding like JS
|
||||
|
||||
SDL_FRect srcRect = {srcX, srcY, srcW, srcH};
|
||||
SDL_FRect dstRect = {x, y, size, size};
|
||||
SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect);
|
||||
}
|
||||
|
||||
void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost) {
|
||||
if (piece.type >= PIECE_COUNT) return;
|
||||
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(piece, cx, cy)) {
|
||||
float px = ox + (piece.x + cx) * tileSize;
|
||||
float py = oy + (piece.y + cy) * tileSize;
|
||||
|
||||
if (isGhost) {
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// Draw ghost piece as barely visible gray outline
|
||||
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 20); // Very faint gray
|
||||
SDL_FRect rect = {px + 2, py + 2, tileSize - 4, tileSize - 4};
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
|
||||
// Draw thin gray border
|
||||
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 30);
|
||||
SDL_FRect border = {px + 1, py + 1, tileSize - 2, tileSize - 2};
|
||||
SDL_RenderRect(renderer, &border);
|
||||
} else {
|
||||
drawBlockTexture(renderer, blocksTex, px, py, tileSize, piece.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize) {
|
||||
if (pieceType >= PIECE_COUNT) return;
|
||||
|
||||
// Use the first rotation (index 0) for preview
|
||||
Game::Piece previewPiece;
|
||||
previewPiece.type = pieceType;
|
||||
previewPiece.rot = 0;
|
||||
previewPiece.x = 0;
|
||||
previewPiece.y = 0;
|
||||
|
||||
// Center the piece in the preview area
|
||||
float offsetX = 0, offsetY = 0;
|
||||
if (pieceType == 0) { offsetX = tileSize * 0.5f; } // I-piece centering (assuming I = 0)
|
||||
else if (pieceType == 1) { offsetX = tileSize * 0.5f; } // O-piece centering (assuming O = 1)
|
||||
|
||||
// Use semi-transparent alpha for preview blocks
|
||||
Uint8 previewAlpha = 180;
|
||||
if (blocksTex) {
|
||||
SDL_SetTextureAlphaMod(blocksTex, previewAlpha);
|
||||
}
|
||||
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(previewPiece, cx, cy)) {
|
||||
float px = x + offsetX + cx * tileSize;
|
||||
float py = y + offsetY + cy * tileSize;
|
||||
drawBlockTexture(renderer, blocksTex, px, py, tileSize, pieceType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset alpha
|
||||
if (blocksTex) {
|
||||
SDL_SetTextureAlphaMod(blocksTex, 255);
|
||||
}
|
||||
}
|
||||
|
||||
void GameRenderer::renderPlayingState(
|
||||
SDL_Renderer* renderer,
|
||||
Game* game,
|
||||
FontAtlas* pixelFont,
|
||||
LineEffect* lineEffect,
|
||||
SDL_Texture* blocksTex,
|
||||
float logicalW,
|
||||
float logicalH,
|
||||
float logicalScale,
|
||||
float winW,
|
||||
float winH,
|
||||
bool showExitConfirmPopup
|
||||
) {
|
||||
if (!game || !pixelFont) return;
|
||||
|
||||
// Calculate actual content area (centered within the window)
|
||||
float contentScale = logicalScale;
|
||||
float contentW = logicalW * contentScale;
|
||||
float contentH = logicalH * contentScale;
|
||||
float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
|
||||
float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
|
||||
|
||||
// Helper lambda for drawing rectangles with content offset
|
||||
auto drawRectWithOffset = [&](float x, float y, float w, float h, SDL_Color c) {
|
||||
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
|
||||
SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h};
|
||||
SDL_RenderFillRect(renderer, &fr);
|
||||
};
|
||||
|
||||
// Responsive layout that scales with window size while maintaining margins
|
||||
const float MIN_MARGIN = 40.0f;
|
||||
const float TOP_MARGIN = 60.0f;
|
||||
const float PANEL_WIDTH = 180.0f;
|
||||
const float PANEL_SPACING = 30.0f;
|
||||
const float NEXT_PIECE_HEIGHT = 120.0f;
|
||||
const float BOTTOM_MARGIN = 60.0f;
|
||||
|
||||
// Calculate layout dimensions
|
||||
const float availableWidth = logicalW - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2);
|
||||
const float availableHeight = logicalH - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PIECE_HEIGHT;
|
||||
|
||||
const float maxBlockSizeW = availableWidth / Game::COLS;
|
||||
const float maxBlockSizeH = availableHeight / Game::ROWS;
|
||||
const float BLOCK_SIZE = std::min(maxBlockSizeW, maxBlockSizeH);
|
||||
const float finalBlockSize = std::max(20.0f, std::min(BLOCK_SIZE, 40.0f));
|
||||
|
||||
const float GRID_W = Game::COLS * finalBlockSize;
|
||||
const float GRID_H = Game::ROWS * finalBlockSize;
|
||||
|
||||
// Calculate positions
|
||||
const float totalContentHeight = NEXT_PIECE_HEIGHT + GRID_H;
|
||||
const float availableVerticalSpace = logicalH - TOP_MARGIN - BOTTOM_MARGIN;
|
||||
const float verticalCenterOffset = (availableVerticalSpace - totalContentHeight) * 0.5f;
|
||||
const float contentStartY = TOP_MARGIN + verticalCenterOffset;
|
||||
|
||||
const float totalLayoutWidth = PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + PANEL_WIDTH;
|
||||
const float layoutStartX = (logicalW - totalLayoutWidth) * 0.5f;
|
||||
|
||||
const float statsX = layoutStartX + contentOffsetX;
|
||||
const float gridX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + contentOffsetX;
|
||||
const float scoreX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + contentOffsetX;
|
||||
const float gridY = contentStartY + NEXT_PIECE_HEIGHT + contentOffsetY;
|
||||
|
||||
const float statsY = gridY;
|
||||
const float statsW = PANEL_WIDTH;
|
||||
const float statsH = GRID_H;
|
||||
|
||||
// Next piece preview position
|
||||
const float nextW = finalBlockSize * 4 + 20;
|
||||
const float nextH = finalBlockSize * 2 + 20;
|
||||
const float nextX = gridX + (GRID_W - nextW) * 0.5f;
|
||||
const float nextY = contentStartY + contentOffsetY;
|
||||
|
||||
// Handle line clearing effects
|
||||
if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) {
|
||||
auto completedLines = game->getCompletedLines();
|
||||
lineEffect->startLineClear(completedLines, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
||||
}
|
||||
|
||||
// Draw game grid border
|
||||
drawRectWithOffset(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255});
|
||||
drawRectWithOffset(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 255});
|
||||
drawRectWithOffset(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255});
|
||||
|
||||
// Draw panel backgrounds
|
||||
SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160);
|
||||
SDL_FRect lbg{statsX - 16, gridY - 10, statsW + 32, GRID_H + 20};
|
||||
SDL_RenderFillRect(renderer, &lbg);
|
||||
|
||||
SDL_FRect rbg{scoreX - 16, gridY - 16, statsW + 32, GRID_H + 32};
|
||||
SDL_RenderFillRect(renderer, &rbg);
|
||||
|
||||
// Draw grid lines
|
||||
SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255);
|
||||
for (int x = 1; x < Game::COLS; ++x) {
|
||||
float lineX = gridX + x * finalBlockSize;
|
||||
SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H);
|
||||
}
|
||||
for (int y = 1; y < Game::ROWS; ++y) {
|
||||
float lineY = gridY + y * finalBlockSize;
|
||||
SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY);
|
||||
}
|
||||
|
||||
// Draw block statistics panel border
|
||||
drawRectWithOffset(statsX - 3 - contentOffsetX, statsY - 3 - contentOffsetY, statsW + 6, statsH + 6, {100, 120, 200, 255});
|
||||
drawRectWithOffset(statsX - contentOffsetX, statsY - contentOffsetY, statsW, statsH, {30, 35, 50, 255});
|
||||
|
||||
// Draw next piece preview panel border
|
||||
drawRectWithOffset(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255});
|
||||
drawRectWithOffset(nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH, {30, 35, 50, 255});
|
||||
|
||||
// Draw the game board
|
||||
const auto &board = game->boardRef();
|
||||
for (int y = 0; y < Game::ROWS; ++y) {
|
||||
for (int x = 0; x < Game::COLS; ++x) {
|
||||
int v = board[y * Game::COLS + x];
|
||||
if (v > 0) {
|
||||
float bx = gridX + x * finalBlockSize;
|
||||
float by = gridY + y * finalBlockSize;
|
||||
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw ghost piece (where current piece will land)
|
||||
if (!game->isPaused()) {
|
||||
Game::Piece ghostPiece = game->current();
|
||||
// Find landing position
|
||||
while (true) {
|
||||
Game::Piece testPiece = ghostPiece;
|
||||
testPiece.y++;
|
||||
bool collision = false;
|
||||
|
||||
// Simple collision check
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(testPiece, cx, cy)) {
|
||||
int gx = testPiece.x + cx;
|
||||
int gy = testPiece.y + cy;
|
||||
if (gy >= Game::ROWS || gx < 0 || gx >= Game::COLS ||
|
||||
(gy >= 0 && board[gy * Game::COLS + gx] != 0)) {
|
||||
collision = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (collision) break;
|
||||
}
|
||||
|
||||
if (collision) break;
|
||||
ghostPiece = testPiece;
|
||||
}
|
||||
|
||||
// Draw ghost piece
|
||||
drawPiece(renderer, blocksTex, ghostPiece, gridX, gridY, finalBlockSize, true);
|
||||
}
|
||||
|
||||
// Draw the falling piece
|
||||
if (!game->isPaused()) {
|
||||
drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false);
|
||||
}
|
||||
|
||||
// Draw line clearing effects
|
||||
if (lineEffect && lineEffect->isActive()) {
|
||||
lineEffect->render(renderer, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
||||
}
|
||||
|
||||
// Draw next piece preview
|
||||
pixelFont->draw(renderer, nextX + 10, nextY - 20, "NEXT", 1.0f, {255, 220, 0, 255});
|
||||
if (game->next().type < PIECE_COUNT) {
|
||||
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(game->next().type), nextX + 10, nextY + 10, finalBlockSize * 0.6f);
|
||||
}
|
||||
|
||||
// Draw block statistics (left panel)
|
||||
pixelFont->draw(renderer, statsX + 10, statsY + 10, "BLOCKS", 1.0f, {255, 220, 0, 255});
|
||||
|
||||
const auto& blockCounts = game->getBlockCounts();
|
||||
int totalBlocks = 0;
|
||||
for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i];
|
||||
|
||||
const char* pieceNames[] = {"I", "O", "T", "S", "Z", "J", "L"};
|
||||
float yCursor = statsY + 52;
|
||||
|
||||
for (int i = 0; i < PIECE_COUNT; ++i) {
|
||||
float py = yCursor;
|
||||
|
||||
// Draw small piece icon
|
||||
float previewSize = finalBlockSize * 0.55f;
|
||||
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(i), statsX + 18, py, previewSize);
|
||||
|
||||
// Compute preview height
|
||||
int maxCy = -1;
|
||||
Game::Piece prev;
|
||||
prev.type = static_cast<PieceType>(i);
|
||||
prev.rot = 0;
|
||||
prev.x = 0;
|
||||
prev.y = 0;
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(prev, cx, cy)) maxCy = std::max(maxCy, cy);
|
||||
}
|
||||
}
|
||||
int tilesHigh = (maxCy >= 0 ? maxCy + 1 : 1);
|
||||
float previewHeight = tilesHigh * previewSize;
|
||||
|
||||
// Count display
|
||||
int count = blockCounts[i];
|
||||
char countStr[16];
|
||||
snprintf(countStr, sizeof(countStr), "%d", count);
|
||||
pixelFont->draw(renderer, statsX + statsW - 20, py + 6, countStr, 1.1f, {240, 240, 245, 255});
|
||||
|
||||
// Percentage bar
|
||||
int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(count) / double(totalBlocks))) : 0;
|
||||
char percStr[16];
|
||||
snprintf(percStr, sizeof(percStr), "%d%%", perc);
|
||||
|
||||
float barX = statsX + 12;
|
||||
float barY = py + previewHeight + 18.0f;
|
||||
float barW = statsW - 24;
|
||||
float barH = 6;
|
||||
|
||||
pixelFont->draw(renderer, barX, barY - 16, percStr, 0.8f, {230, 230, 235, 255});
|
||||
|
||||
// Progress bar
|
||||
SDL_SetRenderDrawColor(renderer, 170, 170, 175, 200);
|
||||
SDL_FRect track{barX, barY, barW, barH};
|
||||
SDL_RenderFillRect(renderer, &track);
|
||||
|
||||
SDL_Color pc = COLORS[i + 1];
|
||||
SDL_SetRenderDrawColor(renderer, pc.r, pc.g, pc.b, 230);
|
||||
float fillW = barW * (perc / 100.0f);
|
||||
if (fillW < 0) fillW = 0;
|
||||
if (fillW > barW) fillW = barW;
|
||||
SDL_FRect fill{barX, barY, fillW, barH};
|
||||
SDL_RenderFillRect(renderer, &fill);
|
||||
|
||||
yCursor = barY + barH + 18.0f;
|
||||
}
|
||||
|
||||
// Draw score panel (right side)
|
||||
const float contentTopOffset = 0.0f;
|
||||
const float contentBottomOffset = 290.0f;
|
||||
const float contentPad = 36.0f;
|
||||
float scoreContentH = (contentBottomOffset - contentTopOffset) + contentPad;
|
||||
float baseY = gridY + (GRID_H - scoreContentH) * 0.5f;
|
||||
|
||||
pixelFont->draw(renderer, scoreX, baseY + 0, "SCORE", 1.0f, {255, 220, 0, 255});
|
||||
char scoreStr[32];
|
||||
snprintf(scoreStr, sizeof(scoreStr), "%d", game->score());
|
||||
pixelFont->draw(renderer, scoreX, baseY + 25, scoreStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
pixelFont->draw(renderer, scoreX, baseY + 70, "LINES", 1.0f, {255, 220, 0, 255});
|
||||
char linesStr[16];
|
||||
snprintf(linesStr, sizeof(linesStr), "%03d", game->lines());
|
||||
pixelFont->draw(renderer, scoreX, baseY + 95, linesStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
pixelFont->draw(renderer, scoreX, baseY + 140, "LEVEL", 1.0f, {255, 220, 0, 255});
|
||||
char levelStr[16];
|
||||
snprintf(levelStr, sizeof(levelStr), "%02d", game->level());
|
||||
pixelFont->draw(renderer, scoreX, baseY + 165, levelStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
// Next level progress
|
||||
int startLv = game->startLevelBase();
|
||||
int firstThreshold = (startLv + 1) * 10;
|
||||
int linesDone = game->lines();
|
||||
int nextThreshold = 0;
|
||||
if (linesDone < firstThreshold) {
|
||||
nextThreshold = firstThreshold;
|
||||
} else {
|
||||
int blocksPast = linesDone - firstThreshold;
|
||||
nextThreshold = firstThreshold + ((blocksPast / 10) + 1) * 10;
|
||||
}
|
||||
int linesForNext = std::max(0, nextThreshold - linesDone);
|
||||
pixelFont->draw(renderer, scoreX, baseY + 200, "NEXT LVL", 1.0f, {255, 220, 0, 255});
|
||||
char nextStr[32];
|
||||
snprintf(nextStr, sizeof(nextStr), "%d LINES", linesForNext);
|
||||
pixelFont->draw(renderer, scoreX, baseY + 225, nextStr, 0.9f, {80, 255, 120, 255});
|
||||
|
||||
// Time display
|
||||
pixelFont->draw(renderer, scoreX, baseY + 265, "TIME", 1.0f, {255, 220, 0, 255});
|
||||
int totalSecs = static_cast<int>(game->elapsed());
|
||||
int mins = totalSecs / 60;
|
||||
int secs = totalSecs % 60;
|
||||
char timeStr[16];
|
||||
snprintf(timeStr, sizeof(timeStr), "%02d:%02d", mins, secs);
|
||||
pixelFont->draw(renderer, scoreX, baseY + 290, timeStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
// Gravity HUD
|
||||
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, logicalW - 260, 10, gms, 0.9f, {200, 200, 220, 255});
|
||||
|
||||
// Hold piece (if implemented)
|
||||
if (game->held().type < PIECE_COUNT) {
|
||||
pixelFont->draw(renderer, statsX + 10, statsY + statsH - 80, "HOLD", 1.0f, {255, 220, 0, 255});
|
||||
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f);
|
||||
}
|
||||
|
||||
// Pause overlay
|
||||
if (game->isPaused() && !showExitConfirmPopup) {
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180);
|
||||
SDL_FRect pauseOverlay{0, 0, logicalW, logicalH};
|
||||
SDL_RenderFillRect(renderer, &pauseOverlay);
|
||||
|
||||
pixelFont->draw(renderer, logicalW * 0.5f - 80, logicalH * 0.5f - 20, "PAUSED", 2.0f, {255, 255, 255, 255});
|
||||
pixelFont->draw(renderer, logicalW * 0.5f - 120, logicalH * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255});
|
||||
}
|
||||
|
||||
// Exit confirmation popup
|
||||
if (showExitConfirmPopup) {
|
||||
float popupW = 420.0f, popupH = 180.0f;
|
||||
float popupX = (logicalW - popupW) * 0.5f;
|
||||
float popupY = (logicalH - popupH) * 0.5f;
|
||||
|
||||
// Dim entire window (do not change viewport/scales here)
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200);
|
||||
SDL_FRect fullWin{0.f, 0.f, winW, winH};
|
||||
SDL_RenderFillRect(renderer, &fullWin);
|
||||
|
||||
// Draw popup box in logical coords with content offsets
|
||||
drawRectWithOffset(popupX - 4.0f, popupY - 4.0f, popupW + 8.0f, popupH + 8.0f, {60, 70, 90, 255});
|
||||
drawRectWithOffset(popupX, popupY, popupW, popupH, {20, 22, 28, 240});
|
||||
|
||||
// Text content (measure to perfectly center)
|
||||
const std::string title = "Exit game?";
|
||||
const std::string line1 = "Are you sure you want to";
|
||||
const std::string line2 = "leave the current game?";
|
||||
|
||||
int wTitle=0,hTitle=0; pixelFont->measure(title, 1.6f, wTitle, hTitle);
|
||||
int wL1=0,hL1=0; pixelFont->measure(line1, 0.9f, wL1, hL1);
|
||||
int wL2=0,hL2=0; pixelFont->measure(line2, 0.9f, wL2, hL2);
|
||||
|
||||
float titleX = popupX + (popupW - (float)wTitle) * 0.5f + contentOffsetX;
|
||||
float l1X = popupX + (popupW - (float)wL1) * 0.5f + contentOffsetX;
|
||||
float l2X = popupX + (popupW - (float)wL2) * 0.5f + contentOffsetX;
|
||||
|
||||
pixelFont->draw(renderer, titleX, popupY + contentOffsetY + 20.0f, title, 1.6f, {255, 220, 0, 255});
|
||||
pixelFont->draw(renderer, l1X, popupY + contentOffsetY + 60.0f, line1, 0.9f, {220, 220, 230, 255});
|
||||
pixelFont->draw(renderer, l2X, popupY + contentOffsetY + 84.0f, line2, 0.9f, {220, 220, 230, 255});
|
||||
|
||||
// Buttons
|
||||
float btnW = 140.0f, btnH = 46.0f;
|
||||
float yesX = popupX + popupW * 0.25f - btnW * 0.5f;
|
||||
float noX = popupX + popupW * 0.75f - btnW * 0.5f;
|
||||
float btnY = popupY + popupH - 60.0f;
|
||||
|
||||
// YES button
|
||||
drawRectWithOffset(yesX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255});
|
||||
drawRectWithOffset(yesX, btnY, btnW, btnH, {200, 60, 60, 255});
|
||||
const std::string yes = "YES";
|
||||
int wYes=0,hYes=0; pixelFont->measure(yes, 1.0f, wYes, hYes);
|
||||
pixelFont->draw(renderer, yesX + (btnW - (float)wYes) * 0.5f + contentOffsetX,
|
||||
btnY + (btnH - (float)hYes) * 0.5f + contentOffsetY,
|
||||
yes, 1.0f, {255, 255, 255, 255});
|
||||
|
||||
// NO button
|
||||
drawRectWithOffset(noX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255});
|
||||
drawRectWithOffset(noX, btnY, btnW, btnH, {80, 140, 80, 255});
|
||||
const std::string no = "NO";
|
||||
int wNo=0,hNo=0; pixelFont->measure(no, 1.0f, wNo, hNo);
|
||||
pixelFont->draw(renderer, noX + (btnW - (float)wNo) * 0.5f + contentOffsetX,
|
||||
btnY + (btnH - (float)hNo) * 0.5f + contentOffsetY,
|
||||
no, 1.0f, {255, 255, 255, 255});
|
||||
}
|
||||
}
|
||||
40
src/graphics/GameRenderer.h
Normal file
40
src/graphics/GameRenderer.h
Normal file
@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
#include "../gameplay/Game.h"
|
||||
|
||||
// Forward declarations
|
||||
class FontAtlas;
|
||||
class LineEffect;
|
||||
|
||||
/**
|
||||
* GameRenderer - Utility class for rendering the Tetris game board and HUD.
|
||||
*
|
||||
* This class encapsulates all the game-specific rendering logic that was
|
||||
* previously in main.cpp, making it reusable across different contexts.
|
||||
*/
|
||||
class GameRenderer {
|
||||
public:
|
||||
// Render the complete playing state including game board, HUD, and effects
|
||||
static void renderPlayingState(
|
||||
SDL_Renderer* renderer,
|
||||
Game* game,
|
||||
FontAtlas* pixelFont,
|
||||
LineEffect* lineEffect,
|
||||
SDL_Texture* blocksTex,
|
||||
float logicalW,
|
||||
float logicalH,
|
||||
float logicalScale,
|
||||
float winW,
|
||||
float winH,
|
||||
bool showExitConfirmPopup
|
||||
);
|
||||
|
||||
private:
|
||||
// Helper functions for drawing game elements
|
||||
static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType);
|
||||
static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false);
|
||||
static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize);
|
||||
|
||||
// Helper function for drawing rectangles
|
||||
static void drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c);
|
||||
};
|
||||
@ -130,6 +130,8 @@ void RenderManager::setViewport(int x, int y, int width, int height) {
|
||||
|
||||
SDL_Rect viewport = { x, y, width, height };
|
||||
SDL_SetRenderViewport(m_renderer, &viewport);
|
||||
// Keep cached logical viewport in sync if this matches our computed logical scale
|
||||
m_logicalVP = viewport;
|
||||
}
|
||||
|
||||
void RenderManager::setScale(float scaleX, float scaleY) {
|
||||
@ -147,6 +149,7 @@ void RenderManager::resetViewport() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset to full window viewport and recompute logical scale/viewport
|
||||
SDL_SetRenderViewport(m_renderer, nullptr);
|
||||
updateLogicalScale();
|
||||
}
|
||||
@ -281,7 +284,15 @@ void RenderManager::updateLogicalScale() {
|
||||
}
|
||||
|
||||
setScale(scale, scale);
|
||||
// Compute centered logical viewport that preserves aspect ratio and is centered in window
|
||||
int vpW = static_cast<int>(m_logicalWidth * scale);
|
||||
int vpH = static_cast<int>(m_logicalHeight * scale);
|
||||
int vpX = (m_windowWidth - vpW) / 2;
|
||||
int vpY = (m_windowHeight - vpH) / 2;
|
||||
SDL_Rect vp{ vpX, vpY, vpW, vpH };
|
||||
SDL_SetRenderViewport(m_renderer, &vp);
|
||||
|
||||
// Set viewport to fill the entire window
|
||||
setViewport(0, 0, m_windowWidth, m_windowHeight);
|
||||
// Cache logical viewport and scale for callers
|
||||
m_logicalVP = vp;
|
||||
m_logicalScale = scale;
|
||||
}
|
||||
|
||||
@ -31,6 +31,10 @@ public:
|
||||
void setScale(float scaleX, float scaleY);
|
||||
void resetViewport();
|
||||
|
||||
// Query the computed logical viewport and scale (useful for consistent input mapping)
|
||||
SDL_Rect getLogicalViewport() const { return m_logicalVP; }
|
||||
float getLogicalScale() const { return m_logicalScale; }
|
||||
|
||||
// Basic rendering operations
|
||||
void clear(Uint8 r = 0, Uint8 g = 0, Uint8 b = 0, Uint8 a = 255);
|
||||
|
||||
@ -75,7 +79,14 @@ private:
|
||||
// State
|
||||
bool m_initialized = false;
|
||||
bool m_isFullscreen = false;
|
||||
// Cached logical viewport and scale (centered within window)
|
||||
SDL_Rect m_logicalVP{0,0,0,0};
|
||||
float m_logicalScale = 1.0f;
|
||||
|
||||
// Helper methods
|
||||
void updateLogicalScale();
|
||||
|
||||
// Query the computed logical viewport and scale (useful for consistent input mapping)
|
||||
SDL_Rect getLogicalViewport() const { return m_logicalVP; }
|
||||
float getLogicalScale() const { return m_logicalScale; }
|
||||
};
|
||||
|
||||
@ -1,23 +1,35 @@
|
||||
// main.cpp - Simplified application entry point
|
||||
// Delegates all application logic to ApplicationManager
|
||||
// main_new.cpp - Simplified application entry point for the refactored build
|
||||
// Sets critical SDL hints (DPI awareness, scaling) before initializing subsystems
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_main.h>
|
||||
#include "core/ApplicationManager.h"
|
||||
#include <iostream>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// Create application manager
|
||||
ApplicationManager app;
|
||||
// Ensure per-monitor DPI awareness so fullscreen/input mapping is correct on high-DPI displays
|
||||
SDL_SetHint("SDL_WINDOWS_DPI_AWARENESS", "permonitorv2");
|
||||
// Keep pixel art crisp when scaling logical content
|
||||
SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "nearest");
|
||||
|
||||
// Initialize the application
|
||||
ApplicationManager app;
|
||||
if (!app.initialize(argc, argv)) {
|
||||
std::cerr << "Failed to initialize application" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Run the main application loop
|
||||
app.run();
|
||||
|
||||
// Application manager destructor will handle cleanup
|
||||
try {
|
||||
app.run();
|
||||
// Ensure orderly teardown before C++ static destruction
|
||||
app.shutdown();
|
||||
} catch (const std::exception& ex) {
|
||||
std::cerr << "Fatal error: " << ex.what() << std::endl;
|
||||
app.shutdown();
|
||||
return 2;
|
||||
} catch (...) {
|
||||
std::cerr << "Unknown fatal error" << std::endl;
|
||||
app.shutdown();
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -187,15 +187,10 @@ void LevelSelectorState::handleEvent(const SDL_Event& e) {
|
||||
if (ctx.startLevelSelection) *ctx.startLevelSelection = hoveredLevel;
|
||||
} else if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
if (e.button.button == SDL_BUTTON_LEFT) {
|
||||
// compute visible logical viewport
|
||||
float vw = (lastLogicalScale > 0.f) ? float(lastLogicalVP.w) / lastLogicalScale : float(LOGICAL_W);
|
||||
float vh = (lastLogicalScale > 0.f) ? float(lastLogicalVP.h) / lastLogicalScale : float(LOGICAL_H);
|
||||
float offX = 0.f;
|
||||
if (lastLogicalScale > 0.f) offX = (vw / 2.f) - (float(LOGICAL_W) / 2.f);
|
||||
// convert mouse to logical coords
|
||||
// convert mouse to logical coords (viewport is already centered)
|
||||
float lx = (float(e.button.x) - float(lastLogicalVP.x)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f);
|
||||
float ly = (float(e.button.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f);
|
||||
SDL_FRect panel = DrawPanel(nullptr, vw, vh, /*draw=*/false, offX, 0.f);
|
||||
SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
|
||||
Grid g = MakeGrid(panel);
|
||||
int hit = HitTest(g, int(lx), int(ly));
|
||||
if (hit != -1) {
|
||||
@ -205,14 +200,10 @@ void LevelSelectorState::handleEvent(const SDL_Event& e) {
|
||||
}
|
||||
}
|
||||
} else if (e.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
// compute visible logical viewport and convert mouse coords once
|
||||
float vw = (lastLogicalScale > 0.f) ? float(lastLogicalVP.w) / lastLogicalScale : float(LOGICAL_W);
|
||||
float vh = (lastLogicalScale > 0.f) ? float(lastLogicalVP.h) / lastLogicalScale : float(LOGICAL_H);
|
||||
float offX = 0.f;
|
||||
if (lastLogicalScale > 0.f) offX = (vw / 2.f) - (float(LOGICAL_W) / 2.f);
|
||||
// convert mouse to logical coords (viewport is already centered)
|
||||
float lx = (float(e.motion.x) - float(lastLogicalVP.x)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f);
|
||||
float ly = (float(e.motion.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f);
|
||||
SDL_FRect panel = DrawPanel(nullptr, vw, vh, /*draw=*/false, offX, 0.f);
|
||||
SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
|
||||
Grid g = MakeGrid(panel);
|
||||
hoveredLevel = HitTest(g, int(lx), int(ly));
|
||||
}
|
||||
@ -233,18 +224,11 @@ void LevelSelectorState::render(SDL_Renderer* renderer, float logicalScale, SDL_
|
||||
void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer) {
|
||||
if (!renderer) return;
|
||||
|
||||
// Important: main sets viewport + logical scale before calling us.
|
||||
// Draw in logical coordinates to avoid artifacts in fullscreen.
|
||||
// Use the actual visible logical size (viewport in logical coords) instead of hardcoded constants
|
||||
float vw = (lastLogicalScale > 0.f) ? float(lastLogicalVP.w) / lastLogicalScale : float(LOGICAL_W);
|
||||
float vh = (lastLogicalScale > 0.f) ? float(lastLogicalVP.h) / lastLogicalScale : float(LOGICAL_H);
|
||||
|
||||
// compute horizontal offset so content centers within the visible logical viewport
|
||||
float offX = 0.f;
|
||||
if (lastLogicalScale > 0.f) {
|
||||
float visibleLogicalW = float(lastLogicalVP.w) / lastLogicalScale;
|
||||
offX = (visibleLogicalW / 2.f) - (vw / 2.f);
|
||||
}
|
||||
// Since ApplicationManager sets up a centered viewport, we draw directly in logical coordinates
|
||||
// The viewport (LOGICAL_W x LOGICAL_H) is already centered within the window
|
||||
float vw = float(LOGICAL_W);
|
||||
float vh = float(LOGICAL_H);
|
||||
float offX = 0.f; // No offset needed since viewport is centered
|
||||
|
||||
// Panel and title strip (in logical space)
|
||||
SDL_FRect panel = DrawPanel(renderer, vw, vh-140.0f, /*draw=*/true, offX, 0.f);
|
||||
@ -268,19 +252,14 @@ void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer) {
|
||||
}
|
||||
|
||||
bool LevelSelectorState::isMouseInPopup(float mouseX, float mouseY, float& popupX, float& popupY, float& popupW, float& popupH) {
|
||||
// Re-implement using new panel geometry in window coordinates
|
||||
// Convert from window pixels to logical coords using cached viewport/scale
|
||||
// Simplified: viewport is already centered, just convert mouse to logical coords
|
||||
(void)mouseX; (void)mouseY;
|
||||
float lx = 0.f, ly = 0.f;
|
||||
if (lastLogicalScale > 0.0f) {
|
||||
lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale;
|
||||
ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale;
|
||||
}
|
||||
float vw = (lastLogicalScale > 0.f) ? float(lastLogicalVP.w) / lastLogicalScale : float(LOGICAL_W);
|
||||
float vh = (lastLogicalScale > 0.f) ? float(lastLogicalVP.h) / lastLogicalScale : float(LOGICAL_H);
|
||||
float offX = 0.f;
|
||||
if (lastLogicalScale > 0.f) offX = (vw / 2.f) - (float(LOGICAL_W) / 2.f);
|
||||
SDL_FRect p = DrawPanel(nullptr, vw, vh, /*draw=*/false, offX, 0.f);
|
||||
SDL_FRect p = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
|
||||
popupX = p.x; popupY = p.y; popupW = p.w; popupH = p.h;
|
||||
return lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH;
|
||||
}
|
||||
@ -292,11 +271,7 @@ int LevelSelectorState::getLevelFromMouse(float mouseX, float mouseY, float popu
|
||||
lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale;
|
||||
ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale;
|
||||
}
|
||||
float vw = (lastLogicalScale > 0.f) ? float(lastLogicalVP.w) / lastLogicalScale : float(LOGICAL_W);
|
||||
float vh = (lastLogicalScale > 0.f) ? float(lastLogicalVP.h) / lastLogicalScale : float(LOGICAL_H);
|
||||
float offX = 0.f;
|
||||
if (lastLogicalScale > 0.f) offX = (vw / 2.f) - (float(LOGICAL_W) / 2.f);
|
||||
SDL_FRect p = DrawPanel(nullptr, vw, vh, /*draw=*/false, offX, 0.f);
|
||||
SDL_FRect p = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
|
||||
Grid g = MakeGrid(p);
|
||||
return HitTest(g, (int)lx, (int)ly);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include "persistence/Scores.h"
|
||||
#include "graphics/Font.h"
|
||||
#include "../core/GlobalState.h"
|
||||
#include "../audio/Audio.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
@ -42,14 +43,10 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
{
|
||||
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render entry\n"); fclose(f); }
|
||||
}
|
||||
// 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;
|
||||
// Since ApplicationManager sets up a centered viewport, we draw directly in logical coordinates
|
||||
// No additional content offset is needed - the viewport itself handles centering
|
||||
float contentOffsetX = 0.0f;
|
||||
float contentOffsetY = 0.0f;
|
||||
|
||||
// Background is drawn by main (stretched to the full window) to avoid double-draw.
|
||||
|
||||
@ -119,16 +116,17 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
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});
|
||||
}
|
||||
|
||||
// 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 };
|
||||
|
||||
for (size_t i = 0; i < maxDisplay; ++i)
|
||||
{
|
||||
float baseY = scoresStartY + i * 25;
|
||||
float wave = std::sin((float)GlobalState::instance().logoAnimCounter * 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});
|
||||
@ -150,6 +148,8 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
}
|
||||
|
||||
// Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test)
|
||||
// Since we removed content offsets, calculate contentW directly from the scale and logical size
|
||||
float contentW = LOGICAL_W * logicalScale;
|
||||
bool isSmall = (contentW < 700.0f);
|
||||
float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f;
|
||||
float btnH = isSmall ? 60.0f : 70.0f;
|
||||
|
||||
@ -3,12 +3,18 @@
|
||||
#include "gameplay/Game.h"
|
||||
#include "gameplay/LineEffect.h"
|
||||
#include "persistence/Scores.h"
|
||||
#include "../audio/Audio.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
PlayingState::PlayingState(StateContext& ctx) : State(ctx) {}
|
||||
|
||||
void PlayingState::onEnter() {
|
||||
// Nothing yet; main still owns game creation
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Entering Playing state");
|
||||
// Initialize the game with the selected starting level
|
||||
if (ctx.game && ctx.startLevelSelection) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection);
|
||||
ctx.game->reset(*ctx.startLevelSelection);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayingState::onExit() {
|
||||
@ -54,12 +60,40 @@ void PlayingState::handleEvent(const SDL_Event& e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Other gameplay keys already registered by main's Playing handler for now
|
||||
// Tetris controls (only when not paused)
|
||||
if (!ctx.game->isPaused()) {
|
||||
// Rotation (still event-based for precise timing)
|
||||
if (e.key.scancode == SDL_SCANCODE_UP || e.key.scancode == SDL_SCANCODE_W ||
|
||||
e.key.scancode == SDL_SCANCODE_Z) {
|
||||
ctx.game->rotate(1); // Clockwise rotation
|
||||
return;
|
||||
}
|
||||
if (e.key.scancode == SDL_SCANCODE_X) {
|
||||
ctx.game->rotate(-1); // Counter-clockwise rotation
|
||||
return;
|
||||
}
|
||||
|
||||
// Hard drop (space)
|
||||
if (e.key.scancode == SDL_SCANCODE_SPACE) {
|
||||
ctx.game->hardDrop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Left/Right movement and soft drop are now handled by
|
||||
// ApplicationManager's update handler for proper DAS/ARR timing
|
||||
}
|
||||
|
||||
void PlayingState::update(double frameMs) {
|
||||
if (!ctx.game) return;
|
||||
|
||||
static bool debugPrinted = false;
|
||||
if (!debugPrinted) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Starting updates, frameMs=%.2f, paused=%d", frameMs, ctx.game->isPaused());
|
||||
debugPrinted = true;
|
||||
}
|
||||
|
||||
// forward per-frame gameplay updates (gravity, elapsed)
|
||||
if (!ctx.game->isPaused()) {
|
||||
ctx.game->tickGravity(frameMs);
|
||||
@ -71,11 +105,8 @@ void PlayingState::update(double frameMs) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
// Note: Game over detection and state transition is now handled by ApplicationManager
|
||||
}
|
||||
|
||||
void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
|
||||
|
||||
@ -40,6 +40,8 @@ struct StateContext {
|
||||
// Audio / SFX - forward declared types in main
|
||||
// Pointers to booleans/flags used by multiple states
|
||||
bool* musicEnabled = nullptr;
|
||||
bool* musicStarted = nullptr;
|
||||
bool* musicLoaded = nullptr;
|
||||
int* startLevelSelection = nullptr;
|
||||
int* hoveredButton = nullptr;
|
||||
// Menu popups (exposed from main)
|
||||
|
||||
Reference in New Issue
Block a user