diff --git a/assets/images/blocks90px_002.png b/assets/images/blocks90px_002.png new file mode 100644 index 0000000..feb5a49 Binary files /dev/null and b/assets/images/blocks90px_002.png differ diff --git a/assets/images/blocks90px_003.png b/assets/images/blocks90px_003.png new file mode 100644 index 0000000..6311f2c Binary files /dev/null and b/assets/images/blocks90px_003.png differ diff --git a/assets/images/hold_panel.png b/assets/images/hold_panel.png new file mode 100644 index 0000000..191ef4a Binary files /dev/null and b/assets/images/hold_panel.png differ diff --git a/assets/images/blocks001.bmp b/assets/images/old/blocks001.bmp similarity index 100% rename from assets/images/blocks001.bmp rename to assets/images/old/blocks001.bmp diff --git a/assets/images/blocks001.png b/assets/images/old/blocks001.png similarity index 100% rename from assets/images/blocks001.png rename to assets/images/old/blocks001.png diff --git a/assets/images/blocks3.bmp b/assets/images/old/blocks3.bmp similarity index 100% rename from assets/images/blocks3.bmp rename to assets/images/old/blocks3.bmp diff --git a/assets/images/blocks3.png b/assets/images/old/blocks3.png similarity index 100% rename from assets/images/blocks3.png rename to assets/images/old/blocks3.png diff --git a/assets/images/blocks90px_001.bmp b/assets/images/old/blocks90px_001.bmp similarity index 100% rename from assets/images/blocks90px_001.bmp rename to assets/images/old/blocks90px_001.bmp diff --git a/src/app/AssetLoader.cpp b/src/app/AssetLoader.cpp index fdec338..95d060e 100644 --- a/src/app/AssetLoader.cpp +++ b/src/app/AssetLoader.cpp @@ -77,7 +77,11 @@ bool AssetLoader::performStep() { m_errors.push_back(std::string("CreateTexture failed: ") + fullPath); } else { std::lock_guard lk(m_texturesMutex); - m_textures[path] = tex; + auto& slot = m_textures[path]; + if (slot && slot != tex) { + SDL_DestroyTexture(slot); + } + slot = tex; } } @@ -95,6 +99,19 @@ bool AssetLoader::performStep() { } } +void AssetLoader::adoptTexture(const std::string& path, SDL_Texture* texture) { + if (!texture) { + return; + } + + std::lock_guard lk(m_texturesMutex); + auto& slot = m_textures[path]; + if (slot && slot != texture) { + SDL_DestroyTexture(slot); + } + slot = texture; +} + float AssetLoader::getProgress() const { int total = m_totalTasks.load(std::memory_order_relaxed); if (total <= 0) return 1.0f; diff --git a/src/app/AssetLoader.h b/src/app/AssetLoader.h index 0d871e5..fac6128 100644 --- a/src/app/AssetLoader.h +++ b/src/app/AssetLoader.h @@ -39,6 +39,10 @@ public: // Get a loaded texture (or nullptr if not loaded). SDL_Texture* getTexture(const std::string& path) const; + // Adopt an externally-created texture so AssetLoader owns its lifetime. + // If a texture is already registered for this path, it will be replaced. + void adoptTexture(const std::string& path, SDL_Texture* texture); + // Return currently-loading path (empty when idle). std::string getCurrentLoading() const; diff --git a/src/app/TetrisApp.cpp b/src/app/TetrisApp.cpp index dc9d8bc..d3cef9e 100644 --- a/src/app/TetrisApp.cpp +++ b/src/app/TetrisApp.cpp @@ -139,6 +139,7 @@ struct TetrisApp::Impl { SDL_Texture* scorePanelTex = nullptr; SDL_Texture* statisticsPanelTex = nullptr; SDL_Texture* nextPanelTex = nullptr; + SDL_Texture* holdPanelTex = nullptr; BackgroundManager levelBackgrounds; int startLevelSelection = 0; @@ -973,7 +974,8 @@ void TetrisApp::Impl::runLoop() "assets/images/blocks90px_001.bmp", "assets/images/panel_score.png", "assets/images/statistics_panel.png", - "assets/images/next_panel.png" + "assets/images/next_panel.png", + "assets/images/hold_panel.png" }; for (auto &p : queuedPaths) { loadingManager->queueTexture(p); @@ -1007,10 +1009,11 @@ void TetrisApp::Impl::runLoop() logoTex = assetLoader.getTexture("assets/images/spacetris.png"); logoSmallTex = assetLoader.getTexture("assets/images/spacetris.png"); mainScreenTex = assetLoader.getTexture("assets/images/main_screen.png"); - blocksTex = assetLoader.getTexture("assets/images/blocks90px_001.bmp"); + blocksTex = assetLoader.getTexture("assets/images/blocks90px_001.png"); scorePanelTex = assetLoader.getTexture("assets/images/panel_score.png"); statisticsPanelTex = assetLoader.getTexture("assets/images/statistics_panel.png"); nextPanelTex = assetLoader.getTexture("assets/images/next_panel.png"); + holdPanelTex = assetLoader.getTexture("assets/images/hold_panel.png"); auto ensureTextureSize = [&](SDL_Texture* tex, int& outW, int& outH) { if (!tex) return; @@ -1027,17 +1030,22 @@ void TetrisApp::Impl::runLoop() auto legacyLoad = [&](const std::string& p, SDL_Texture*& outTex, int* outW = nullptr, int* outH = nullptr) { if (!outTex) { - outTex = textureLoader->loadFromImage(renderer, p, outW, outH); + SDL_Texture* loaded = textureLoader->loadFromImage(renderer, p, outW, outH); + if (loaded) { + outTex = loaded; + assetLoader.adoptTexture(p, loaded); + } } }; legacyLoad("assets/images/spacetris.png", logoTex); legacyLoad("assets/images/spacetris.png", logoSmallTex, &logoSmallW, &logoSmallH); legacyLoad("assets/images/main_screen.png", mainScreenTex, &mainScreenW, &mainScreenH); - legacyLoad("assets/images/blocks90px_001.bmp", blocksTex); + legacyLoad("assets/images/blocks90px_001.png", blocksTex); legacyLoad("assets/images/panel_score.png", scorePanelTex); legacyLoad("assets/images/statistics_panel.png", statisticsPanelTex); legacyLoad("assets/images/next_panel.png", nextPanelTex); + legacyLoad("assets/images/hold_panel.png", holdPanelTex); if (!blocksTex) { blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90); @@ -1051,6 +1059,9 @@ void TetrisApp::Impl::runLoop() SDL_RenderFillRect(renderer, &rect); } SDL_SetRenderTarget(renderer, nullptr); + + // Ensure the generated fallback texture is cleaned up with other assets. + assetLoader.adoptTexture("assets/images/blocks90px_001.png", blocksTex); } if (musicLoaded) { @@ -1192,6 +1203,7 @@ void TetrisApp::Impl::runLoop() ctx.scorePanelTex = scorePanelTex; ctx.statisticsPanelTex = statisticsPanelTex; ctx.nextPanelTex = nextPanelTex; + ctx.holdPanelTex = holdPanelTex; ctx.mainScreenTex = mainScreenTex; ctx.mainScreenW = mainScreenW; ctx.mainScreenH = mainScreenH; @@ -1417,7 +1429,14 @@ void TetrisApp::Impl::runLoop() break; case AppState::Menu: if (!mainScreenTex) { - mainScreenTex = textureLoader->loadFromImage(renderer, "assets/images/main_screen.png", &mainScreenW, &mainScreenH); + mainScreenTex = assetLoader.getTexture("assets/images/main_screen.png"); + } + if (!mainScreenTex) { + SDL_Texture* loaded = textureLoader->loadFromImage(renderer, "assets/images/main_screen.png", &mainScreenW, &mainScreenH); + if (loaded) { + assetLoader.adoptTexture("assets/images/main_screen.png", loaded); + mainScreenTex = loaded; + } } if (menuState) { menuState->drawMainButtonNormally = false; @@ -1490,6 +1509,7 @@ void TetrisApp::Impl::runLoop() ctx.statisticsPanelTex, scorePanelTex, nextPanelTex, + holdPanelTex, (float)LOGICAL_W, (float)LOGICAL_H, logicalScale, @@ -1669,27 +1689,18 @@ void TetrisApp::Impl::shutdown() { Settings::instance().save(); - if (logoTex) { - SDL_DestroyTexture(logoTex); - logoTex = nullptr; - } - if (mainScreenTex) { - SDL_DestroyTexture(mainScreenTex); - mainScreenTex = nullptr; - } + // BackgroundManager owns its own textures. levelBackgrounds.reset(); - if (blocksTex) { - SDL_DestroyTexture(blocksTex); - blocksTex = nullptr; - } - if (scorePanelTex) { - SDL_DestroyTexture(scorePanelTex); - scorePanelTex = nullptr; - } - if (logoSmallTex) { - SDL_DestroyTexture(logoSmallTex); - logoSmallTex = nullptr; - } + + // All textures are owned by AssetLoader (including legacy fallbacks adopted above). + logoTex = nullptr; + logoSmallTex = nullptr; + backgroundTex = nullptr; + mainScreenTex = nullptr; + blocksTex = nullptr; + scorePanelTex = nullptr; + statisticsPanelTex = nullptr; + nextPanelTex = nullptr; if (scoreLoader.joinable()) { scoreLoader.join(); @@ -1704,6 +1715,11 @@ void TetrisApp::Impl::shutdown() lineEffect.shutdown(); Audio::instance().shutdown(); SoundEffectManager::instance().shutdown(); + + // Destroy textures before tearing down the renderer/window. + assetLoader.shutdown(); + + pixelFont.shutdown(); font.shutdown(); TTF_Quit(); diff --git a/src/core/Config.h b/src/core/Config.h index 83c2193..9d8ea3d 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -130,7 +130,7 @@ namespace Config { constexpr const char* LOGO_BMP = "assets/images/logo.bmp"; constexpr const char* LOGO_SMALL_BMP = "assets/images/logo_small.bmp"; constexpr const char* BACKGROUND_BMP = "assets/images/main_background.bmp"; - constexpr const char* BLOCKS_BMP = "assets/images/blocks90px_001.bmp"; + constexpr const char* BLOCKS_BMP = "assets/images/2.png"; } // Audio settings diff --git a/src/core/application/ApplicationManager.cpp b/src/core/application/ApplicationManager.cpp index 528bfd5..bd22541 100644 --- a/src/core/application/ApplicationManager.cpp +++ b/src/core/application/ApplicationManager.cpp @@ -1165,6 +1165,7 @@ void ApplicationManager::setupStateHandlers() { m_stateContext.statisticsPanelTex, m_stateContext.scorePanelTex, m_stateContext.nextPanelTex, + m_stateContext.holdPanelTex, LOGICAL_W, LOGICAL_H, logicalScale, diff --git a/src/graphics/GameRenderer.cpp b/src/graphics/GameRenderer.cpp index 4a5f339..fd27828 100644 --- a/src/graphics/GameRenderer.cpp +++ b/src/graphics/GameRenderer.cpp @@ -125,6 +125,7 @@ void GameRenderer::renderPlayingState( SDL_Texture* statisticsPanelTex, SDL_Texture* scorePanelTex, SDL_Texture* nextPanelTex, + SDL_Texture* holdPanelTex, float logicalW, float logicalH, float logicalScale, @@ -466,10 +467,76 @@ void GameRenderer::renderPlayingState( 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(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f); + // Hold panel (always visible): draw background & label; preview shown only when a piece is held. + { + float holdBlockH = (finalBlockSize * 0.6f) * 4.0f; + // Base panel height; enforce minimum but allow larger to fit texture + float panelH = std::max(holdBlockH + 12.0f, 420.0f); + // Increase height by ~20% of the hold block to give more vertical room + float extraH = holdBlockH * 0.50f; + panelH += extraH; + const float holdGap = 18.0f; + + // Align X to the bottom score label (`scoreX`) plus an offset to the right + float panelX = scoreX + 30.0f; // move ~30px right to align with score label + float panelW = statsW + 32.0f; + float panelY = gridY - panelH - holdGap; + // Move panel a bit higher for spacing (about half the extra height) + panelY -= extraH * 0.5f; + float labelX = panelX + 40.0f; // shift HOLD label ~30px to the right + float labelY = panelY + 8.0f; + + if (holdPanelTex) { + int texW = 0, texH = 0; + SDL_QueryTexture(holdPanelTex, nullptr, nullptr, &texW, &texH); + if (texW > 0 && texH > 0) { + // If the texture is taller than the current panel, expand panelH + float texAspect = float(texH) / float(texW); + float desiredTexH = panelW * texAspect; + if (desiredTexH + 12.0f > panelH) { + panelH = desiredTexH + 12.0f; + // Recompute vertical placement after growing panelH + panelY = gridY - panelH - holdGap; + labelY = panelY + 8.0f; + } + + // Fill panel width and compute destination height from texture aspect ratio + float texAspect = float(texH) / float(texW); + float dstW = panelW; + float dstH = dstW * texAspect * 1.2f; + // If texture height exceeds panel, expand panelH to fit texture comfortably + if (dstH + 12.0f > panelH) { + panelH = dstH + 12.0f; + panelY = gridY - panelH - holdGap; + labelY = panelY + 8.0f; + } + float dstX = panelX; + float dstY = panelY + (panelH - dstH) * 0.5f; + + SDL_FRect panelDst{dstX, dstY, dstW, dstH}; + SDL_SetTextureBlendMode(holdPanelTex, SDL_BLENDMODE_BLEND); + SDL_SetTextureScaleMode(holdPanelTex, SDL_SCALEMODE_LINEAR); + SDL_RenderTexture(renderer, holdPanelTex, nullptr, &panelDst); + } else { + // Fallback to filling panel area if texture metrics unavailable + SDL_SetRenderDrawColor(renderer, 12, 18, 32, 220); + SDL_FRect panelDst{panelX, panelY, panelW, panelH}; + SDL_RenderFillRect(renderer, &panelDst); + } + } else { + SDL_SetRenderDrawColor(renderer, 12, 18, 32, 220); + SDL_FRect panelDst{panelX, panelY, panelW, panelH}; + SDL_RenderFillRect(renderer, &panelDst); + } + + pixelFont->draw(renderer, labelX, labelY, "HOLDx", 1.0f, {255, 220, 0, 255}); + + if (game->held().type < PIECE_COUNT) { + float previewW = finalBlockSize * 0.6f * 4.0f; + float previewX = panelX + (panelW - previewW) * 0.5f; + float previewY = panelY + (panelH - holdBlockH) * 0.5f; + drawSmallPiece(renderer, blocksTex, static_cast(game->held().type), previewX, previewY, finalBlockSize * 0.6f); + } } // Pause overlay (suppressed when requested, e.g., countdown) diff --git a/src/graphics/GameRenderer.h b/src/graphics/GameRenderer.h index 5b074da..a677748 100644 --- a/src/graphics/GameRenderer.h +++ b/src/graphics/GameRenderer.h @@ -24,6 +24,7 @@ public: SDL_Texture* statisticsPanelTex, SDL_Texture* scorePanelTex, SDL_Texture* nextPanelTex, + SDL_Texture* holdPanelTex, float logicalW, float logicalH, float logicalScale, diff --git a/src/graphics/challenge_mode.md b/src/graphics/challenge_mode.md new file mode 100644 index 0000000..97716ab --- /dev/null +++ b/src/graphics/challenge_mode.md @@ -0,0 +1,287 @@ +# Spacetris — Challenge Mode (Asteroids) Implementation Spec for VS Code AI Agent + +> Goal: Implement/extend **CHALLENGE** gameplay in Spacetris (not a separate mode), based on 100 levels with **asteroid** prefilled blocks that must be destroyed to advance. + +--- + +## 1) High-level Requirements + +### Modes +- Existing mode remains **ENDLESS**. +- Add/extend **CHALLENGE** mode with **100 levels**. + +### Core Challenge Loop +- Each level starts with **prefilled obstacle blocks** called **Asteroids**. +- **Level N** starts with **N asteroids** (placed increasingly higher as level increases). +- Player advances to the next level when **ALL asteroids are destroyed**. +- Gravity (and optionally lock pressure) increases per level. + +### Asteroid concept +Asteroids are special blocks placed into the grid at level start: +- They are **not** player-controlled pieces. +- They have **types** and **hit points** (how many times they must be cleared via line clears). + +--- + +## 2) Asteroid Types & Rules + +Define asteroid types and their behavior: + +### A) Normal Asteroid +- `hitsRemaining = 1` +- Removed when its row is cleared once. +- Never moves (no gravity). + +### B) Armored Asteroid +- `hitsRemaining = 2` +- On first line clear that includes it: decrement hits and change to cracked visual state. +- On second clear: removed. +- Never moves (no gravity). + +### C) Falling Asteroid +- `hitsRemaining = 2` +- On first clear: decrement hits, then **becomes gravity-enabled** (drops until resting). +- On second clear: removed. + +### D) Core Asteroid (late levels) +- `hitsRemaining = 3` +- On each clear: decrement hits and change visual state. +- After first hit (or after any hit — choose consistent rule) it becomes gravity-enabled. +- On final clear: removed (optionally trigger bigger VFX). + +**Important:** These are all within the same CHALLENGE mode. + +--- + +## 3) Level Progression Rules (100 Levels) + +### Asteroid Count +- `asteroidsToPlace = level` (Level 1 -> 1 asteroid, Level 2 -> 2 asteroids, …) +- Recommendation for implementation safety: + - If `level` becomes too large to place comfortably, still place `level` but distribute across more rows and allow overlaps only if empty. + - If needed, implement a soft cap for placement attempts (avoid infinite loops). If cannot place all, place as many as possible and log/telemetry. + +### Placement Height / Region +- Early levels: place in bottom 2–4 rows. +- Mid levels: bottom 6–10 rows. +- Late levels: up to ~half board height. +- Use a function to define a `minRow..maxRow` region based on `level`. + +Example guidance: +- `maxRow = boardHeight - 1` +- `minRow = boardHeight - 1 - clamp(2 + level/3, 2, boardHeight/2)` + +### Type Distribution by Level (suggested) +- Levels 1–9: Normal only +- Levels 10–19: add Armored (small %) +- Levels 20–59: add Falling (increasing %) +- Levels 60–100: add Core (increasing %) + +--- + +## 4) Difficulty Scaling + +### Gravity Speed Scaling +Implement per-level gravity scale: +- `gravity = baseGravity * (1.0f + level * 0.02f)` (tune) +- Or use a curve/table. + +Optional additional scaling: +- Reduced lock delay slightly at higher levels +- Slightly faster DAS/ARR (if implemented) + +--- + +## 5) Win/Lose Conditions + +### Level Completion +- Level completes when: `asteroidsRemaining == 0` +- Then: + - Clear board (or keep board — choose one consistent behavior; recommended: **clear board** for clean progression). + - Show short transition (optional). + - Load next level, until level 100. +- After level 100 completion: show completion screen + stats. + +### Game Over +- Standard Tetris game over: stack reaches spawn/top (existing behavior). + +--- + +## 6) Rendering / UI Requirements + +### Visual Differentiation +Asteroids must be visually distinct from normal tetromino blocks. + +Provide visual states: +- Normal: rock texture +- Armored: plated / darker +- Cracked: visible cracks +- Falling: glow rim / hazard stripes +- Core: pulsing inner core + +Minimum UI additions (Challenge): +- Display `LEVEL: X/100` +- Display `ASTEROIDS REMAINING: N` (or an icon counter) + +--- + +## 7) Data Structures (C++ Guidance) + +### Cell Representation +Each grid cell must store: +- Whether occupied +- If occupied: is it part of normal tetromino or an asteroid +- If asteroid: type + hitsRemaining + gravityEnabled + visualState + +Suggested enums: +```cpp +enum class CellKind { Empty, Tetromino, Asteroid }; + +enum class AsteroidType { Normal, Armored, Falling, Core }; + +struct AsteroidCell { + AsteroidType type; + uint8_t hitsRemaining; + bool gravityEnabled; + uint8_t visualState; // optional (e.g. 0..n) +}; + +struct Cell { + CellKind kind; + // For Tetromino: color/type id + // For Asteroid: AsteroidCell data +}; +```` + +--- + +## 8) Line Clear Processing Rules (Important) + +When a line is cleared: + +1. Detect full rows (existing). +2. For each cleared row: + + * For each cell: + + * If `kind == Asteroid`: + + * `hitsRemaining--` + * If `hitsRemaining == 0`: remove (cell becomes Empty) + * Else: + + * Update its visual state (cracked/damaged) + * If asteroid type is Falling/Core and rule says it becomes gravity-enabled on first hit: + + * `gravityEnabled = true` +3. After clearing rows and collapsing the grid: + + * Apply **asteroid gravity step**: + + * For all gravity-enabled asteroid cells: let them fall until resting. + * Ensure stable iteration (bottom-up scan). +4. Recount asteroids remaining; if 0 -> level complete. + +**Note:** Decide whether gravity-enabled asteroids fall immediately after the first hit (recommended) and whether they fall as individual cells (recommended) or as clusters (optional later). + +--- + +## 9) Asteroid Gravity Algorithm (Simple + Stable) + +Implement a pass: + +* Iterate from bottom-2 to top (bottom-up). +* If cell is gravity-enabled asteroid and below is empty: + + * Move down by one +* Repeat passes until no movement OR do a while-loop per cell to drop fully. + +Be careful to avoid skipping cells when moving: + +* Use bottom-up iteration and drop-to-bottom logic. + +--- + +## 10) Level Generation (Deterministic Option) + +To make challenge reproducible: + +* Use a seed: `seed = baseSeed + level` +* Place asteroids with RNG based on level seed. + +Placement constraints: + +* Avoid placing asteroids in the spawn zone/top rows. +* Avoid creating impossible scenarios too early: + + * For early levels, ensure at least one vertical shaft exists. + +--- + +## 11) Tasks Checklist for AI Agent + +### A) Add Challenge Level System + +* [ ] Add `currentLevel (1..100)` and `mode == CHALLENGE`. +* [ ] Add `StartChallengeLevel(level)` function. +* [ ] Reset/prepare board state for each level (recommended: clear board). + +### B) Asteroid Placement + +* [ ] Implement `PlaceAsteroids(level)`: + + * Determine region of rows + * Choose type distribution + * Place `level` asteroid cells into empty spots + +### C) Line Clear Hook + +* [ ] Modify existing line clear code: + + * Apply asteroid hit logic + * Update visuals + * Enable gravity where required + +### D) Gravity-enabled Asteroids + +* [ ] Implement `ApplyAsteroidGravity()` after line clears and board collapse. + +### E) Level Completion + +* [ ] Track `asteroidsRemaining`. +* [ ] When 0: trigger level transition and `StartChallengeLevel(level+1)`. + +### F) UI + +* [ ] Add level & asteroids remaining display. + +--- + +## 12) Acceptance Criteria + +* Level 1 spawns exactly 1 asteroid. +* Level N spawns N asteroids. +* Destroying asteroids requires: + + * Normal: 1 clear + * Armored: 2 clears + * Falling: 2 clears + becomes gravity-enabled after first hit + * Core: 3 clears (+ gravity-enabled rule) +* Player advances only when all asteroids are destroyed. +* Gravity increases by level and is clearly noticeable by mid-levels. +* No infinite loops in placement or gravity. +* Challenge works end-to-end through level 100. + +--- + +## 13) Notes / Tuning Hooks + +Expose tuning constants: + +* `baseGravity` +* `gravityPerLevel` +* `minAsteroidRow(level)` +* `typeDistribution(level)` weights +* `coreGravityOnHit` rule + +--- \ No newline at end of file diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 326c767..f1feabd 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -518,6 +518,7 @@ void GameRenderer::renderPlayingState( SDL_Texture* statisticsPanelTex, SDL_Texture* scorePanelTex, SDL_Texture* nextPanelTex, + SDL_Texture* holdPanelTex, float logicalW, float logicalH, float logicalScale, @@ -1403,30 +1404,49 @@ void GameRenderer::renderPlayingState( pixelFont->draw(renderer, logicalW - 260, 10, gravityHud, 0.9f, {200, 200, 220, 255}); } - // Hold piece (right side, above score dashboard) - if (game->held().type < PIECE_COUNT) { + // Hold panel background & label (always visible). Small preview renders only if a piece is held. + { float holdLabelX = statsTextX; float holdY = statsY + statsH - 80.0f; + float holdBlockH = (finalBlockSize * 0.6f) * 6.0f; + const float holdGap = 18.0f; + float panelW = 120.0f; + float panelH = holdBlockH + 12.0f; + float panelX = holdLabelX + 40.0f; + float panelY = holdY - 6.0f; + if (scorePanelMetricsValid) { - const float holdGap = 18.0f; - const float holdBlockH = (finalBlockSize * 0.6f) * 4.0f; - holdY = scorePanelTop - holdBlockH - holdGap; - holdLabelX = statsTextX; - // Ensure HOLD block doesn't drift too far left if the score panel gets narrow. - holdLabelX = std::max(holdLabelX, scorePanelLeftX + 14.0f); - // If the score panel is extremely narrow, keep within its bounds. - holdLabelX = std::min(holdLabelX, scorePanelLeftX + std::max(0.0f, scorePanelWidth - 90.0f)); + // align panel to score panel width and position it above it + panelW = scorePanelWidth; + panelX = scorePanelLeftX; + panelY = scorePanelTop - panelH - holdGap; + // choose label X (left edge + padding) + holdLabelX = panelX + 10.0f; + // label Y inside panel + holdY = panelY + 8.0f; } - pixelFont->draw(renderer, holdLabelX, holdY, "HOLD", 1.0f, {255, 220, 0, 255}); - drawSmallPiece( - renderer, - blocksTex, - static_cast(game->held().type), - holdLabelX + 50.0f, - holdY + 2.0f, - finalBlockSize * 0.6f - ); + if (holdPanelTex) { + SDL_FRect panelDst{panelX, panelY, panelW, panelH}; + SDL_SetTextureBlendMode(holdPanelTex, SDL_BLENDMODE_BLEND); + SDL_SetTextureScaleMode(holdPanelTex, SDL_SCALEMODE_LINEAR); + SDL_RenderTexture(renderer, holdPanelTex, nullptr, &panelDst); + } else { + // fallback: draw a dark panel rect so UI is visible even without texture + SDL_SetRenderDrawColor(renderer, 12, 18, 32, 220); + SDL_FRect panelDst{panelX, panelY, panelW, panelH}; + SDL_RenderFillRect(renderer, &panelDst); + } + + // Display "HOLD" label on right side + pixelFont->draw(renderer, holdLabelX + 56.0f, holdY + 4.0f, "HOLD", 1.0f, {255, 220, 0, 255}); + + if (game->held().type < PIECE_COUNT) { + // Draw small held preview inside the panel (centered) + float previewX = panelX + (panelW - (finalBlockSize * 0.6f * 4.0f)) * 0.5f; + float previewY = panelY + (panelH - holdBlockH) * 2.5f; + drawSmallPiece(renderer, blocksTex, static_cast(game->held().type), previewX, previewY, finalBlockSize * 0.6f); + } } // Pause overlay logic moved to renderPauseOverlay diff --git a/src/graphics/renderers/GameRenderer.h b/src/graphics/renderers/GameRenderer.h index 14fd746..c4c730e 100644 --- a/src/graphics/renderers/GameRenderer.h +++ b/src/graphics/renderers/GameRenderer.h @@ -24,6 +24,7 @@ public: SDL_Texture* statisticsPanelTex, SDL_Texture* scorePanelTex, SDL_Texture* nextPanelTex, + SDL_Texture* holdPanelTex, float logicalW, float logicalH, float logicalScale, diff --git a/src/states/PlayingState.cpp b/src/states/PlayingState.cpp index e4de7c0..f055ffb 100644 --- a/src/states/PlayingState.cpp +++ b/src/states/PlayingState.cpp @@ -238,6 +238,7 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l ctx.statisticsPanelTex, ctx.scorePanelTex, ctx.nextPanelTex, + ctx.holdPanelTex, 1200.0f, // LOGICAL_W 1000.0f, // LOGICAL_H logicalScale, @@ -325,6 +326,7 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l ctx.statisticsPanelTex, ctx.scorePanelTex, ctx.nextPanelTex, + ctx.holdPanelTex, 1200.0f, 1000.0f, logicalScale, diff --git a/src/states/State.h b/src/states/State.h index 6aa99e6..36a7a95 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -43,6 +43,7 @@ struct StateContext { SDL_Texture* scorePanelTex = nullptr; SDL_Texture* statisticsPanelTex = nullptr; SDL_Texture* nextPanelTex = nullptr; + SDL_Texture* holdPanelTex = nullptr; // Background for the HOLD preview SDL_Texture* mainScreenTex = nullptr; int mainScreenW = 0; int mainScreenH = 0;