added transition from levels

This commit is contained in:
2025-11-22 21:58:02 +01:00
parent 77a9237e25
commit 05423b4aeb
2 changed files with 211 additions and 174 deletions

View File

@ -106,6 +106,105 @@ static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::stri
return texture; return texture;
} }
struct LevelBackgroundFader {
SDL_Texture* currentTex = nullptr;
SDL_Texture* nextTex = nullptr;
int currentLevel = -1;
int queuedLevel = -1;
float fadeElapsedMs = 0.0f;
float fadeDurationMs = 3500.0f;
};
static void destroyTexture(SDL_Texture*& tex) {
if (tex) {
SDL_DestroyTexture(tex);
tex = nullptr;
}
}
static bool queueLevelBackground(LevelBackgroundFader& fader, SDL_Renderer* renderer, int level) {
if (!renderer) {
return false;
}
level = std::clamp(level, 0, 32);
if (fader.currentLevel == level || fader.queuedLevel == level) {
return true;
}
char bgPath[256];
std::snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.jpg", level);
SDL_Texture* newTexture = loadTextureFromImage(renderer, bgPath);
if (!newTexture) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to queue background for level %d: %s", level, bgPath);
return false;
}
destroyTexture(fader.nextTex);
fader.nextTex = newTexture;
fader.queuedLevel = level;
fader.fadeElapsedMs = 0.0f;
if (!fader.currentTex) {
fader.currentTex = fader.nextTex;
fader.currentLevel = fader.queuedLevel;
fader.nextTex = nullptr;
fader.queuedLevel = -1;
}
return true;
}
static void updateLevelBackgroundFade(LevelBackgroundFader& fader, float frameMs) {
if (!fader.currentTex || !fader.nextTex) {
return;
}
fader.fadeElapsedMs += frameMs;
if (fader.fadeElapsedMs >= fader.fadeDurationMs) {
destroyTexture(fader.currentTex);
fader.currentTex = fader.nextTex;
fader.currentLevel = fader.queuedLevel;
fader.nextTex = nullptr;
fader.queuedLevel = -1;
fader.fadeElapsedMs = 0.0f;
}
}
static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Renderer* renderer, int winW, int winH) {
if (!renderer) {
return;
}
SDL_FRect fullRect{0.f, 0.f, static_cast<float>(winW), static_cast<float>(winH)};
if (fader.currentTex && fader.nextTex) {
const float duration = std::max(1.0f, fader.fadeDurationMs);
const float alpha = std::clamp(fader.fadeElapsedMs / duration, 0.0f, 1.0f);
SDL_SetTextureAlphaMod(fader.currentTex, Uint8((1.0f - alpha) * 255.0f));
SDL_RenderTexture(renderer, fader.currentTex, nullptr, &fullRect);
SDL_SetTextureAlphaMod(fader.currentTex, 255);
SDL_SetTextureAlphaMod(fader.nextTex, Uint8(alpha * 255.0f));
SDL_RenderTexture(renderer, fader.nextTex, nullptr, &fullRect);
SDL_SetTextureAlphaMod(fader.nextTex, 255);
} else if (fader.currentTex) {
SDL_RenderTexture(renderer, fader.currentTex, nullptr, &fullRect);
} else if (fader.nextTex) {
SDL_RenderTexture(renderer, fader.nextTex, nullptr, &fullRect);
}
}
static void resetLevelBackgrounds(LevelBackgroundFader& fader) {
destroyTexture(fader.currentTex);
destroyTexture(fader.nextTex);
fader.currentLevel = -1;
fader.queuedLevel = -1;
fader.fadeElapsedMs = 0.0f;
}
// Hover state for level popup ( -1 = none, 0..19 = hovered level ) // Hover state for level popup ( -1 = none, 0..19 = hovered level )
// Now managed by LevelSelectorState // Now managed by LevelSelectorState
@ -501,12 +600,7 @@ int main(int, char **)
// States should render using `ctx.backgroundTex` rather than accessing globals. // States should render using `ctx.backgroundTex` rather than accessing globals.
// Level background caching system // Level background caching system
SDL_Texture *levelBackgroundTex = nullptr; LevelBackgroundFader levelBackgrounds;
SDL_Texture *nextLevelBackgroundTex = nullptr; // used during fade transitions
int cachedLevel = -1; // Track which level background is currently cached
float levelFadeAlpha = 0.0f; // 0..1 blend factor where 1 means next fully visible
const float LEVEL_FADE_DURATION = 3500.0f; // ms for fade transition (3.5s)
float levelFadeElapsed = 0.0f;
// Default start level selection: 0 (declare here so it's in scope for all handlers) // Default start level selection: 0 (declare here so it's in scope for all handlers)
int startLevelSelection = 0; int startLevelSelection = 0;
@ -1180,10 +1274,7 @@ int main(int, char **)
} }
// Advance level background fade if a next texture is queued // Advance level background fade if a next texture is queued
if (nextLevelBackgroundTex) { updateLevelBackgroundFade(levelBackgrounds, float(frameMs));
levelFadeElapsed += float(frameMs);
levelFadeAlpha = std::min(1.0f, levelFadeElapsed / LEVEL_FADE_DURATION);
}
// Update intro animations // Update intro animations
if (state == AppState::Menu) { if (state == AppState::Menu) {
@ -1276,58 +1367,9 @@ int main(int, char **)
// Draw level-based background for gameplay, starfield for other states // Draw level-based background for gameplay, starfield for other states
if (state == AppState::Playing) { if (state == AppState::Playing) {
// Use level-based background for gameplay with caching int bgLevel = std::clamp(game.level(), 0, 32);
int currentLevel = game.level(); queueLevelBackground(levelBackgrounds, renderer, bgLevel);
int bgLevel = (currentLevel > 32) ? 32 : currentLevel; // Cap at level 32 renderLevelBackgrounds(levelBackgrounds, renderer, winW, winH);
// Only load new background if level changed
if (cachedLevel != bgLevel) {
// Load new level background into nextLevelBackgroundTex
if (nextLevelBackgroundTex) { SDL_DestroyTexture(nextLevelBackgroundTex); nextLevelBackgroundTex = nullptr; }
char bgPath[256];
snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.jpg", bgLevel);
SDL_Texture* newLevelTex = loadTextureFromImage(renderer, bgPath);
if (newLevelTex) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded background for level %d: %s", bgLevel, bgPath);
nextLevelBackgroundTex = newLevelTex;
// start fade transition
levelFadeAlpha = 0.0f;
levelFadeElapsed = 0.0f;
cachedLevel = bgLevel;
} else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load background for level %d: %s", bgLevel, bgPath);
// don't change textures if file missing
cachedLevel = -1;
}
}
// Draw blended backgrounds if needed
if (levelBackgroundTex || nextLevelBackgroundTex) {
// Use actual window pixel size so backgrounds always cover full screen
SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH };
// if fade in progress
if (nextLevelBackgroundTex && levelFadeAlpha < 1.0f && levelBackgroundTex) {
// draw current with inverse alpha
SDL_SetTextureAlphaMod(levelBackgroundTex, Uint8((1.0f - levelFadeAlpha) * 255));
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
SDL_SetTextureAlphaMod(nextLevelBackgroundTex, Uint8(levelFadeAlpha * 255));
SDL_RenderTexture(renderer, nextLevelBackgroundTex, nullptr, &fullRect);
// reset mods
SDL_SetTextureAlphaMod(levelBackgroundTex, 255);
SDL_SetTextureAlphaMod(nextLevelBackgroundTex, 255);
}
else if (nextLevelBackgroundTex && (!levelBackgroundTex || levelFadeAlpha >= 1.0f)) {
// finalise swap
if (levelBackgroundTex) { SDL_DestroyTexture(levelBackgroundTex); }
levelBackgroundTex = nextLevelBackgroundTex;
nextLevelBackgroundTex = nullptr;
levelFadeAlpha = 0.0f;
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
}
else if (levelBackgroundTex) {
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
}
}
} else if (state == AppState::Loading) { } else if (state == AppState::Loading) {
// Use 3D starfield for loading screen (full screen) // Use 3D starfield for loading screen (full screen)
starfield3D.draw(renderer); starfield3D.draw(renderer);
@ -1648,10 +1690,7 @@ int main(int, char **)
SDL_DestroyTexture(logoTex); SDL_DestroyTexture(logoTex);
if (backgroundTex) if (backgroundTex)
SDL_DestroyTexture(backgroundTex); SDL_DestroyTexture(backgroundTex);
if (nextLevelBackgroundTex) resetLevelBackgrounds(levelBackgrounds);
SDL_DestroyTexture(nextLevelBackgroundTex);
if (levelBackgroundTex)
SDL_DestroyTexture(levelBackgroundTex);
if (blocksTex) if (blocksTex)
SDL_DestroyTexture(blocksTex); SDL_DestroyTexture(blocksTex);
if (logoSmallTex) if (logoSmallTex)

View File

@ -105,6 +105,105 @@ static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::stri
return texture; return texture;
} }
struct LevelBackgroundFader {
SDL_Texture* currentTex = nullptr;
SDL_Texture* nextTex = nullptr;
int currentLevel = -1;
int queuedLevel = -1;
float fadeElapsedMs = 0.0f;
float fadeDurationMs = 3500.0f;
};
static void destroyTexture(SDL_Texture*& tex) {
if (tex) {
SDL_DestroyTexture(tex);
tex = nullptr;
}
}
static bool queueLevelBackground(LevelBackgroundFader& fader, SDL_Renderer* renderer, int level) {
if (!renderer) {
return false;
}
level = std::clamp(level, 0, 32);
if (fader.currentLevel == level || fader.queuedLevel == level) {
return true;
}
char bgPath[256];
std::snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.jpg", level);
SDL_Texture* newTexture = loadTextureFromImage(renderer, bgPath);
if (!newTexture) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to queue background for level %d: %s", level, bgPath);
return false;
}
destroyTexture(fader.nextTex);
fader.nextTex = newTexture;
fader.queuedLevel = level;
fader.fadeElapsedMs = 0.0f;
if (!fader.currentTex) {
fader.currentTex = fader.nextTex;
fader.currentLevel = fader.queuedLevel;
fader.nextTex = nullptr;
fader.queuedLevel = -1;
}
return true;
}
static void updateLevelBackgroundFade(LevelBackgroundFader& fader, float frameMs) {
if (!fader.currentTex || !fader.nextTex) {
return;
}
fader.fadeElapsedMs += frameMs;
if (fader.fadeElapsedMs >= fader.fadeDurationMs) {
destroyTexture(fader.currentTex);
fader.currentTex = fader.nextTex;
fader.currentLevel = fader.queuedLevel;
fader.nextTex = nullptr;
fader.queuedLevel = -1;
fader.fadeElapsedMs = 0.0f;
}
}
static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Renderer* renderer, int winW, int winH) {
if (!renderer) {
return;
}
SDL_FRect fullRect{0.f, 0.f, static_cast<float>(winW), static_cast<float>(winH)};
if (fader.currentTex && fader.nextTex) {
const float duration = std::max(1.0f, fader.fadeDurationMs);
const float alpha = std::clamp(fader.fadeElapsedMs / duration, 0.0f, 1.0f);
SDL_SetTextureAlphaMod(fader.currentTex, Uint8((1.0f - alpha) * 255.0f));
SDL_RenderTexture(renderer, fader.currentTex, nullptr, &fullRect);
SDL_SetTextureAlphaMod(fader.currentTex, 255);
SDL_SetTextureAlphaMod(fader.nextTex, Uint8(alpha * 255.0f));
SDL_RenderTexture(renderer, fader.nextTex, nullptr, &fullRect);
SDL_SetTextureAlphaMod(fader.nextTex, 255);
} else if (fader.currentTex) {
SDL_RenderTexture(renderer, fader.currentTex, nullptr, &fullRect);
} else if (fader.nextTex) {
SDL_RenderTexture(renderer, fader.nextTex, nullptr, &fullRect);
}
}
static void resetLevelBackgrounds(LevelBackgroundFader& fader) {
destroyTexture(fader.currentTex);
destroyTexture(fader.nextTex);
fader.currentLevel = -1;
fader.queuedLevel = -1;
fader.fadeElapsedMs = 0.0f;
}
// Hover state for level popup ( -1 = none, 0..19 = hovered level ) // Hover state for level popup ( -1 = none, 0..19 = hovered level )
// Now managed by LevelSelectorState // Now managed by LevelSelectorState
@ -495,12 +594,7 @@ int main(int, char **)
// States should render using `ctx.backgroundTex` rather than accessing globals. // States should render using `ctx.backgroundTex` rather than accessing globals.
// Level background caching system // Level background caching system
SDL_Texture *levelBackgroundTex = nullptr; LevelBackgroundFader levelBackgrounds;
SDL_Texture *nextLevelBackgroundTex = nullptr; // used during fade transitions
int cachedLevel = -1; // Track which level background is currently cached
float levelFadeAlpha = 0.0f; // 0..1 blend factor where 1 means next fully visible
const float LEVEL_FADE_DURATION = 3500.0f; // ms for fade transition (3.5s)
float levelFadeElapsed = 0.0f;
// Default start level selection: 0 (declare here so it's in scope for all handlers) // Default start level selection: 0 (declare here so it's in scope for all handlers)
int startLevelSelection = 0; int startLevelSelection = 0;
@ -815,7 +909,6 @@ int main(int, char **)
exitPopupSelectedButton = 1; exitPopupSelectedButton = 1;
} }
// Settings button (gear icon area - top right)
SDL_FRect settingsBtn{LOGICAL_W - 60, 10, 50, 30}; SDL_FRect settingsBtn{LOGICAL_W - 60, 10, 50, 30};
if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h) if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h)
{ {
@ -830,48 +923,6 @@ int main(int, char **)
stateMgr.setState(state); stateMgr.setState(state);
} }
else if (state == AppState::Playing && showExitConfirmPopup) { else if (state == AppState::Playing && showExitConfirmPopup) {
// Convert mouse to logical coordinates and to content-local coords
float lx = (mx - logicalVP.x) / logicalScale;
float ly = (my - logicalVP.y) / logicalScale;
// Compute content offsets (same as in render path)
float contentW = LOGICAL_W * logicalScale;
float contentH = LOGICAL_H * logicalScale;
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
// Map to content-local logical coords (what drawing code uses)
float localX = lx - contentOffsetX;
float localY = ly - contentOffsetY;
// Popup rect in logical coordinates (content-local)
float popupW = 400, popupH = 200;
float popupX = (LOGICAL_W - popupW) / 2.0f;
float popupY = (LOGICAL_H - popupH) / 2.0f;
// Simple Yes/No buttons
float btnW = 120.0f, btnH = 40.0f;
float yesX = popupX + popupW * 0.25f - btnW / 2.0f;
float noX = popupX + popupW * 0.75f - btnW / 2.0f;
float btnY = popupY + popupH - btnH - 20.0f;
if (localX >= popupX && localX <= popupX + popupW && localY >= popupY && localY <= popupY + popupH) {
// Click inside popup - check buttons
if (localX >= yesX && localX <= yesX + btnW && localY >= btnY && localY <= btnY + btnH) {
// Yes -> go back to menu
showExitConfirmPopup = false;
game.reset(startLevelSelection);
state = AppState::Menu;
stateMgr.setState(state);
} else if (localX >= noX && localX <= noX + btnW && localY >= btnY && localY <= btnY + btnH) {
// No -> close popup and resume
showExitConfirmPopup = false;
game.setPaused(false);
}
} else {
// Click outside popup: cancel
showExitConfirmPopup = false;
game.setPaused(false);
}
}
else if (state == AppState::Menu && showExitConfirmPopup) {
float contentW = LOGICAL_W * logicalScale; float contentW = LOGICAL_W * logicalScale;
float contentH = LOGICAL_H * logicalScale; float contentH = LOGICAL_H * logicalScale;
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
@ -1088,10 +1139,7 @@ int main(int, char **)
} }
// Advance level background fade if a next texture is queued // Advance level background fade if a next texture is queued
if (nextLevelBackgroundTex) { updateLevelBackgroundFade(levelBackgrounds, float(frameMs));
levelFadeElapsed += float(frameMs);
levelFadeAlpha = std::min(1.0f, levelFadeElapsed / LEVEL_FADE_DURATION);
}
// Update intro animations // Update intro animations
if (state == AppState::Menu) { if (state == AppState::Menu) {
@ -1124,56 +1172,9 @@ int main(int, char **)
// Draw level-based background for gameplay, starfield for other states // Draw level-based background for gameplay, starfield for other states
if (state == AppState::Playing) { if (state == AppState::Playing) {
// Use level-based background for gameplay with caching int bgLevel = std::clamp(game.level(), 0, 32);
int currentLevel = game.level(); queueLevelBackground(levelBackgrounds, renderer, bgLevel);
int bgLevel = (currentLevel > 32) ? 32 : currentLevel; // Cap at level 32 renderLevelBackgrounds(levelBackgrounds, renderer, winW, winH);
// Only load new background if level changed
if (cachedLevel != bgLevel) {
// Load new level background into nextLevelBackgroundTex
if (nextLevelBackgroundTex) { SDL_DestroyTexture(nextLevelBackgroundTex); nextLevelBackgroundTex = nullptr; }
char bgPath[256];
snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.jpg", bgLevel);
SDL_Texture* newLevelTex = loadTextureFromImage(renderer, bgPath);
if (newLevelTex) {
nextLevelBackgroundTex = newLevelTex;
// start fade transition
levelFadeAlpha = 0.0f;
levelFadeElapsed = 0.0f;
cachedLevel = bgLevel;
} else {
// don't change textures if file missing
cachedLevel = -1;
}
}
// Draw blended backgrounds if needed
if (levelBackgroundTex || nextLevelBackgroundTex) {
// Use actual window pixel size so backgrounds always cover full screen
SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH };
// if fade in progress
if (nextLevelBackgroundTex && levelFadeAlpha < 1.0f && levelBackgroundTex) {
// draw current with inverse alpha
SDL_SetTextureAlphaMod(levelBackgroundTex, Uint8((1.0f - levelFadeAlpha) * 255));
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
SDL_SetTextureAlphaMod(nextLevelBackgroundTex, Uint8(levelFadeAlpha * 255));
SDL_RenderTexture(renderer, nextLevelBackgroundTex, nullptr, &fullRect);
// reset mods
SDL_SetTextureAlphaMod(levelBackgroundTex, 255);
SDL_SetTextureAlphaMod(nextLevelBackgroundTex, 255);
}
else if (nextLevelBackgroundTex && (!levelBackgroundTex || levelFadeAlpha >= 1.0f)) {
// finalise swap
if (levelBackgroundTex) { SDL_DestroyTexture(levelBackgroundTex); }
levelBackgroundTex = nextLevelBackgroundTex;
nextLevelBackgroundTex = nullptr;
levelFadeAlpha = 0.0f;
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
}
else if (levelBackgroundTex) {
SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect);
}
}
} else if (state == AppState::Loading) { } else if (state == AppState::Loading) {
// Use 3D starfield for loading screen (full screen) // Use 3D starfield for loading screen (full screen)
starfield3D.draw(renderer); starfield3D.draw(renderer);
@ -1714,10 +1715,7 @@ int main(int, char **)
SDL_DestroyTexture(logoTex); SDL_DestroyTexture(logoTex);
if (backgroundTex) if (backgroundTex)
SDL_DestroyTexture(backgroundTex); SDL_DestroyTexture(backgroundTex);
if (nextLevelBackgroundTex) resetLevelBackgrounds(levelBackgrounds);
SDL_DestroyTexture(nextLevelBackgroundTex);
if (levelBackgroundTex)
SDL_DestroyTexture(levelBackgroundTex);
if (blocksTex) if (blocksTex)
SDL_DestroyTexture(blocksTex); SDL_DestroyTexture(blocksTex);
if (logoSmallTex) if (logoSmallTex)