Compare commits
10 Commits
fd29ae271e
...
18c29fed1e
| Author | SHA1 | Date | |
|---|---|---|---|
| 18c29fed1e | |||
| f3064e9dad | |||
| a1f16a7d94 | |||
| eb9822dac7 | |||
| 6c48af0bec | |||
| b69b090e45 | |||
| ad014e1de0 | |||
| 9a3c1a0688 | |||
| 970259e3d6 | |||
| 34447f0245 |
@ -1,363 +0,0 @@
|
|||||||
# Tetris SDL3 - Improvements Checklist
|
|
||||||
|
|
||||||
Quick reference for implementing the recommendations from CODE_ANALYSIS.md
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔴 High Priority (Critical)
|
|
||||||
|
|
||||||
### 1. Smart Pointer Wrapper for SDL Resources
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 2-3 hours
|
|
||||||
**Impact:** Prevents memory leaks, improves safety
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Create `src/utils/SDLPointers.h` with smart pointer wrappers
|
|
||||||
- [ ] Replace raw `SDL_Texture*` in `MenuState.h` (lines 17-21)
|
|
||||||
- [ ] Replace raw `SDL_Texture*` in `PlayingState.h`
|
|
||||||
- [ ] Update `main.cpp` texture loading
|
|
||||||
- [ ] Test all states to ensure no regressions
|
|
||||||
|
|
||||||
**Code Template:**
|
|
||||||
```cpp
|
|
||||||
// src/utils/SDLPointers.h
|
|
||||||
#pragma once
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
struct SDL_TextureDeleter {
|
|
||||||
void operator()(SDL_Texture* tex) const {
|
|
||||||
if (tex) SDL_DestroyTexture(tex);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SDL_SurfaceDeleter {
|
|
||||||
void operator()(SDL_Surface* surf) const {
|
|
||||||
if (surf) SDL_DestroySurface(surf);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
using SDL_TexturePtr = std::unique_ptr<SDL_Texture, SDL_TextureDeleter>;
|
|
||||||
using SDL_SurfacePtr = std::unique_ptr<SDL_Surface, SDL_SurfaceDeleter>;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Remove Debug File I/O
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 30 minutes
|
|
||||||
**Impact:** Performance, code cleanliness
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Remove or wrap `fopen("tetris_trace.log")` calls in `MenuState.cpp`
|
|
||||||
- [ ] Remove or wrap similar calls in other files
|
|
||||||
- [ ] Replace with SDL_LogTrace or conditional compilation
|
|
||||||
- [ ] Delete `tetris_trace.log` from repository
|
|
||||||
|
|
||||||
**Files to Update:**
|
|
||||||
- `src/states/MenuState.cpp` (lines 182-184, 195-203, 277-278, 335-337)
|
|
||||||
- `src/main.cpp` (if any similar patterns exist)
|
|
||||||
|
|
||||||
**Replacement Pattern:**
|
|
||||||
```cpp
|
|
||||||
// Before:
|
|
||||||
FILE* f = fopen("tetris_trace.log", "a");
|
|
||||||
if (f) { fprintf(f, "MenuState::render entry\n"); fclose(f); }
|
|
||||||
|
|
||||||
// After:
|
|
||||||
SDL_LogTrace(SDL_LOG_CATEGORY_APPLICATION, "MenuState::render entry");
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Improve Error Handling in Asset Loading
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 2 hours
|
|
||||||
**Impact:** Better debugging, prevents crashes
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Update `loadTextureFromImage` to return error information
|
|
||||||
- [ ] Add validation for all asset loads in `main.cpp`
|
|
||||||
- [ ] Create fallback assets for missing resources
|
|
||||||
- [ ] Add startup asset validation
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```cpp
|
|
||||||
struct AssetLoadResult {
|
|
||||||
SDL_TexturePtr texture;
|
|
||||||
std::string error;
|
|
||||||
bool success;
|
|
||||||
};
|
|
||||||
|
|
||||||
AssetLoadResult loadTextureFromImage(SDL_Renderer* renderer,
|
|
||||||
const std::string& path);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🟡 Medium Priority (Important)
|
|
||||||
|
|
||||||
### 4. Extract Common Patterns
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 3-4 hours
|
|
||||||
**Impact:** Reduces code duplication
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Create `ExitPopupHelper` class in `StateContext.h`
|
|
||||||
- [ ] Update `MenuState.cpp` to use helper
|
|
||||||
- [ ] Update `PlayingState.cpp` to use helper
|
|
||||||
- [ ] Update `OptionsState.cpp` to use helper
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. Move Magic Numbers to Config.h
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 1 hour
|
|
||||||
**Impact:** Maintainability
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Add menu button constants to `Config::UI`
|
|
||||||
- [ ] Add rendering constants to appropriate namespace
|
|
||||||
- [ ] Update `MenuState.cpp` to use config constants
|
|
||||||
- [ ] Update `UIRenderer.cpp` to use config constants
|
|
||||||
|
|
||||||
**Constants to Add:**
|
|
||||||
```cpp
|
|
||||||
namespace Config::UI {
|
|
||||||
constexpr float MENU_BUTTON_WIDTH = 200.0f;
|
|
||||||
constexpr float MENU_BUTTON_HEIGHT = 70.0f;
|
|
||||||
constexpr float MENU_BUTTON_Y_FRACTION = 0.865f;
|
|
||||||
constexpr float MENU_BUTTON_SPACING = 210.0f;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. Add Unit Tests
|
|
||||||
**Status:** ⚠️ Minimal (only GravityTests)
|
|
||||||
**Effort:** 8-10 hours
|
|
||||||
**Impact:** Code quality, regression prevention
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Create `tests/GameLogicTests.cpp`
|
|
||||||
- [ ] Test piece spawning
|
|
||||||
- [ ] Test rotation
|
|
||||||
- [ ] Test collision detection
|
|
||||||
- [ ] Test line clearing
|
|
||||||
- [ ] Test scoring
|
|
||||||
- [ ] Create `tests/ScoreManagerTests.cpp`
|
|
||||||
- [ ] Test score submission
|
|
||||||
- [ ] Test high score detection
|
|
||||||
- [ ] Test persistence
|
|
||||||
- [ ] Create `tests/StateTransitionTests.cpp`
|
|
||||||
- [ ] Test state transitions
|
|
||||||
- [ ] Test state lifecycle (onEnter/onExit)
|
|
||||||
- [ ] Update CMakeLists.txt to include new tests
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🟢 Low Priority (Nice to Have)
|
|
||||||
|
|
||||||
### 7. Add Doxygen Documentation
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 4-6 hours
|
|
||||||
**Impact:** Developer onboarding
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Create `Doxyfile` configuration
|
|
||||||
- [ ] Add class-level documentation to core classes
|
|
||||||
- [ ] Add function-level documentation to public APIs
|
|
||||||
- [ ] Generate HTML documentation
|
|
||||||
- [ ] Add to build process
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8. Performance Profiling
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 4-6 hours
|
|
||||||
**Impact:** Depends on findings
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Profile with Visual Studio Profiler / Instruments
|
|
||||||
- [ ] Identify hotspots
|
|
||||||
- [ ] Optimize critical paths
|
|
||||||
- [ ] Add performance benchmarks
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 9. Standardize Member Variable Naming
|
|
||||||
**Status:** ⚠️ Inconsistent
|
|
||||||
**Effort:** 2-3 hours
|
|
||||||
**Impact:** Code consistency
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Decide on naming convention (recommend `m_` prefix for private members)
|
|
||||||
- [ ] Update all class member variables
|
|
||||||
- [ ] Update documentation to reflect convention
|
|
||||||
|
|
||||||
**Convention Recommendation:**
|
|
||||||
```cpp
|
|
||||||
class Example {
|
|
||||||
public:
|
|
||||||
int publicValue; // No prefix for public members
|
|
||||||
|
|
||||||
private:
|
|
||||||
int m_privateValue; // m_ prefix for private members
|
|
||||||
float m_memberVariable; // Consistent across all classes
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Code Quality Improvements
|
|
||||||
|
|
||||||
### 10. Add .clang-format
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 15 minutes
|
|
||||||
**Impact:** Consistent formatting
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Create `.clang-format` file in project root
|
|
||||||
- [ ] Run formatter on all source files
|
|
||||||
- [ ] Add format check to CI/CD
|
|
||||||
|
|
||||||
**Suggested .clang-format:**
|
|
||||||
```yaml
|
|
||||||
BasedOnStyle: LLVM
|
|
||||||
IndentWidth: 4
|
|
||||||
ColumnLimit: 120
|
|
||||||
PointerAlignment: Left
|
|
||||||
AllowShortFunctionsOnASingleLine: Empty
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 11. Add README.md
|
|
||||||
**Status:** ❌ Missing
|
|
||||||
**Effort:** 1 hour
|
|
||||||
**Impact:** Project documentation
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Create `README.md` with:
|
|
||||||
- [ ] Project description
|
|
||||||
- [ ] Screenshots/GIF
|
|
||||||
- [ ] Build instructions
|
|
||||||
- [ ] Dependencies
|
|
||||||
- [ ] Controls
|
|
||||||
- [ ] License
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 12. Set Up CI/CD
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 2-3 hours
|
|
||||||
**Impact:** Automated testing
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Create `.github/workflows/build.yml`
|
|
||||||
- [ ] Add Windows build job
|
|
||||||
- [ ] Add macOS build job
|
|
||||||
- [ ] Add test execution
|
|
||||||
- [ ] Add artifact upload
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Refactoring Opportunities
|
|
||||||
|
|
||||||
### 13. Create Asset Manager
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 4-5 hours
|
|
||||||
**Impact:** Better resource management
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Create `src/core/assets/AssetManager.h`
|
|
||||||
- [ ] Implement texture caching
|
|
||||||
- [ ] Implement font caching
|
|
||||||
- [ ] Update states to use AssetManager
|
|
||||||
- [ ] Add asset preloading
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 14. Implement Event System
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 6-8 hours
|
|
||||||
**Impact:** Decoupling, flexibility
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Create `src/core/events/EventBus.h`
|
|
||||||
- [ ] Define event types
|
|
||||||
- [ ] Replace callbacks with events
|
|
||||||
- [ ] Update Game class to publish events
|
|
||||||
- [ ] Update Audio system to subscribe to events
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 15. Component-Based UI
|
|
||||||
**Status:** ❌ Not Started
|
|
||||||
**Effort:** 8-10 hours
|
|
||||||
**Impact:** UI maintainability
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Create `src/ui/components/Button.h`
|
|
||||||
- [ ] Create `src/ui/components/Panel.h`
|
|
||||||
- [ ] Create `src/ui/components/Label.h`
|
|
||||||
- [ ] Refactor MenuState to use components
|
|
||||||
- [ ] Refactor OptionsState to use components
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Progress Tracking
|
|
||||||
|
|
||||||
| Category | Total Items | Completed | In Progress | Not Started |
|
|
||||||
|----------|-------------|-----------|-------------|-------------|
|
|
||||||
| High Priority | 3 | 0 | 0 | 3 |
|
|
||||||
| Medium Priority | 3 | 0 | 0 | 3 |
|
|
||||||
| Low Priority | 3 | 0 | 0 | 3 |
|
|
||||||
| Code Quality | 3 | 0 | 0 | 3 |
|
|
||||||
| Refactoring | 3 | 0 | 0 | 3 |
|
|
||||||
| **TOTAL** | **15** | **0** | **0** | **15** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Suggested Implementation Order
|
|
||||||
|
|
||||||
### Week 1: Critical Fixes
|
|
||||||
1. Remove debug file I/O (30 min)
|
|
||||||
2. Smart pointer wrapper (2-3 hours)
|
|
||||||
3. Improve error handling (2 hours)
|
|
||||||
|
|
||||||
### Week 2: Code Quality
|
|
||||||
4. Move magic numbers to Config.h (1 hour)
|
|
||||||
5. Extract common patterns (3-4 hours)
|
|
||||||
6. Add .clang-format (15 min)
|
|
||||||
7. Add README.md (1 hour)
|
|
||||||
|
|
||||||
### Week 3: Testing
|
|
||||||
8. Add GameLogicTests (4 hours)
|
|
||||||
9. Add ScoreManagerTests (2 hours)
|
|
||||||
10. Add StateTransitionTests (2 hours)
|
|
||||||
|
|
||||||
### Week 4: Documentation & CI
|
|
||||||
11. Set up CI/CD (2-3 hours)
|
|
||||||
12. Add Doxygen documentation (4-6 hours)
|
|
||||||
|
|
||||||
### Future Iterations:
|
|
||||||
13. Performance profiling
|
|
||||||
14. Asset Manager
|
|
||||||
15. Event System
|
|
||||||
16. Component-Based UI
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Notes
|
|
||||||
|
|
||||||
- Mark items as completed by changing ❌ to ✅
|
|
||||||
- Update progress table as you complete items
|
|
||||||
- Feel free to reorder based on your priorities
|
|
||||||
- Some items can be done in parallel
|
|
||||||
- Consider creating GitHub issues for tracking
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** 2025-12-03
|
|
||||||
**Next Review:** After completing High Priority items
|
|
||||||
@ -1,774 +0,0 @@
|
|||||||
# Quick Start: Implementing Top 3 Improvements
|
|
||||||
|
|
||||||
This guide provides complete, copy-paste ready code for the three most impactful improvements.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Improvement #1: Smart Pointer Wrapper for SDL Resources
|
|
||||||
|
|
||||||
### Step 1: Create the Utility Header
|
|
||||||
|
|
||||||
**File:** `src/utils/SDLPointers.h`
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#pragma once
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file SDLPointers.h
|
|
||||||
* @brief Smart pointer wrappers for SDL resources
|
|
||||||
*
|
|
||||||
* Provides RAII wrappers for SDL resources to prevent memory leaks
|
|
||||||
* and ensure proper cleanup in all code paths.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace SDL {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Deleter for SDL_Texture
|
|
||||||
*/
|
|
||||||
struct TextureDeleter {
|
|
||||||
void operator()(SDL_Texture* tex) const {
|
|
||||||
if (tex) {
|
|
||||||
SDL_DestroyTexture(tex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Deleter for SDL_Surface
|
|
||||||
*/
|
|
||||||
struct SurfaceDeleter {
|
|
||||||
void operator()(SDL_Surface* surf) const {
|
|
||||||
if (surf) {
|
|
||||||
SDL_DestroySurface(surf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Deleter for SDL_Renderer
|
|
||||||
*/
|
|
||||||
struct RendererDeleter {
|
|
||||||
void operator()(SDL_Renderer* renderer) const {
|
|
||||||
if (renderer) {
|
|
||||||
SDL_DestroyRenderer(renderer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Deleter for SDL_Window
|
|
||||||
*/
|
|
||||||
struct WindowDeleter {
|
|
||||||
void operator()(SDL_Window* window) const {
|
|
||||||
if (window) {
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Smart pointer for SDL_Texture
|
|
||||||
*
|
|
||||||
* Example usage:
|
|
||||||
* @code
|
|
||||||
* SDL::TexturePtr texture(SDL_CreateTexture(...));
|
|
||||||
* if (!texture) {
|
|
||||||
* // Handle error
|
|
||||||
* }
|
|
||||||
* // Automatic cleanup when texture goes out of scope
|
|
||||||
* @endcode
|
|
||||||
*/
|
|
||||||
using TexturePtr = std::unique_ptr<SDL_Texture, TextureDeleter>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Smart pointer for SDL_Surface
|
|
||||||
*/
|
|
||||||
using SurfacePtr = std::unique_ptr<SDL_Surface, SurfaceDeleter>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Smart pointer for SDL_Renderer
|
|
||||||
*/
|
|
||||||
using RendererPtr = std::unique_ptr<SDL_Renderer, RendererDeleter>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Smart pointer for SDL_Window
|
|
||||||
*/
|
|
||||||
using WindowPtr = std::unique_ptr<SDL_Window, WindowDeleter>;
|
|
||||||
|
|
||||||
} // namespace SDL
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 2: Update MenuState.h
|
|
||||||
|
|
||||||
**File:** `src/states/MenuState.h`
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```cpp
|
|
||||||
private:
|
|
||||||
int selectedButton = 0;
|
|
||||||
|
|
||||||
// Button icons (optional - will use text if nullptr)
|
|
||||||
SDL_Texture* playIcon = nullptr;
|
|
||||||
SDL_Texture* levelIcon = nullptr;
|
|
||||||
SDL_Texture* optionsIcon = nullptr;
|
|
||||||
SDL_Texture* exitIcon = nullptr;
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```cpp
|
|
||||||
#include "../utils/SDLPointers.h" // Add this include
|
|
||||||
|
|
||||||
private:
|
|
||||||
int selectedButton = 0;
|
|
||||||
|
|
||||||
// Button icons (optional - will use text if nullptr)
|
|
||||||
SDL::TexturePtr playIcon;
|
|
||||||
SDL::TexturePtr levelIcon;
|
|
||||||
SDL::TexturePtr optionsIcon;
|
|
||||||
SDL::TexturePtr exitIcon;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 3: Update MenuState.cpp
|
|
||||||
|
|
||||||
**File:** `src/states/MenuState.cpp`
|
|
||||||
|
|
||||||
**Remove the manual cleanup from onExit:**
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```cpp
|
|
||||||
void MenuState::onExit() {
|
|
||||||
if (ctx.showExitConfirmPopup) {
|
|
||||||
*ctx.showExitConfirmPopup = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up icon textures
|
|
||||||
if (playIcon) { SDL_DestroyTexture(playIcon); playIcon = nullptr; }
|
|
||||||
if (levelIcon) { SDL_DestroyTexture(levelIcon); levelIcon = nullptr; }
|
|
||||||
if (optionsIcon) { SDL_DestroyTexture(optionsIcon); optionsIcon = nullptr; }
|
|
||||||
if (exitIcon) { SDL_DestroyTexture(exitIcon); exitIcon = nullptr; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```cpp
|
|
||||||
void MenuState::onExit() {
|
|
||||||
if (ctx.showExitConfirmPopup) {
|
|
||||||
*ctx.showExitConfirmPopup = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Icon textures are automatically cleaned up by smart pointers
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Update usage in render method:**
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```cpp
|
|
||||||
std::array<SDL_Texture*, 4> icons = {
|
|
||||||
playIcon,
|
|
||||||
levelIcon,
|
|
||||||
optionsIcon,
|
|
||||||
exitIcon
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```cpp
|
|
||||||
std::array<SDL_Texture*, 4> icons = {
|
|
||||||
playIcon.get(),
|
|
||||||
levelIcon.get(),
|
|
||||||
optionsIcon.get(),
|
|
||||||
exitIcon.get()
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 4: Update main.cpp Texture Loading
|
|
||||||
|
|
||||||
**File:** `src/main.cpp`
|
|
||||||
|
|
||||||
**Update the function signature and implementation:**
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```cpp
|
|
||||||
static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::string& path, int* outW = nullptr, int* outH = nullptr) {
|
|
||||||
if (!renderer) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string resolvedPath = AssetPath::resolveImagePath(path);
|
|
||||||
SDL_Surface* surface = IMG_Load(resolvedPath.c_str());
|
|
||||||
if (!surface) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image %s (resolved: %s): %s", path.c_str(), resolvedPath.c_str(), SDL_GetError());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outW) { *outW = surface->w; }
|
|
||||||
if (outH) { *outH = surface->h; }
|
|
||||||
|
|
||||||
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
|
|
||||||
SDL_DestroySurface(surface);
|
|
||||||
|
|
||||||
if (!texture) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create texture from %s: %s", resolvedPath.c_str(), SDL_GetError());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvedPath != path) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded %s via %s", path.c_str(), resolvedPath.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```cpp
|
|
||||||
#include "utils/SDLPointers.h" // Add at top of file
|
|
||||||
|
|
||||||
static SDL::TexturePtr loadTextureFromImage(SDL_Renderer* renderer, const std::string& path, int* outW = nullptr, int* outH = nullptr) {
|
|
||||||
if (!renderer) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Renderer is null");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string resolvedPath = AssetPath::resolveImagePath(path);
|
|
||||||
SDL::SurfacePtr surface(IMG_Load(resolvedPath.c_str()));
|
|
||||||
if (!surface) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image %s (resolved: %s): %s",
|
|
||||||
path.c_str(), resolvedPath.c_str(), SDL_GetError());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outW) { *outW = surface->w; }
|
|
||||||
if (outH) { *outH = surface->h; }
|
|
||||||
|
|
||||||
SDL::TexturePtr texture(SDL_CreateTextureFromSurface(renderer, surface.get()));
|
|
||||||
// surface is automatically destroyed here
|
|
||||||
|
|
||||||
if (!texture) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create texture from %s: %s",
|
|
||||||
resolvedPath.c_str(), SDL_GetError());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvedPath != path) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded %s via %s", path.c_str(), resolvedPath.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧹 Improvement #2: Remove Debug File I/O
|
|
||||||
|
|
||||||
### Step 1: Replace with SDL Logging
|
|
||||||
|
|
||||||
**File:** `src/states/MenuState.cpp`
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```cpp
|
|
||||||
// Trace entry to persistent log for debugging abrupt exit/crash during render
|
|
||||||
{
|
|
||||||
FILE* f = fopen("tetris_trace.log", "a");
|
|
||||||
if (f) {
|
|
||||||
fprintf(f, "MenuState::render entry\n");
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```cpp
|
|
||||||
// Use SDL's built-in logging (only in debug builds)
|
|
||||||
#ifdef _DEBUG
|
|
||||||
SDL_LogTrace(SDL_LOG_CATEGORY_APPLICATION, "MenuState::render entry");
|
|
||||||
#endif
|
|
||||||
```
|
|
||||||
|
|
||||||
**Or, if you want it always enabled but less verbose:**
|
|
||||||
```cpp
|
|
||||||
SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, "MenuState::render entry");
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 2: Create a Logging Utility (Optional, Better Approach)
|
|
||||||
|
|
||||||
**File:** `src/utils/Logger.h`
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#pragma once
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Centralized logging utility
|
|
||||||
*
|
|
||||||
* Wraps SDL logging with compile-time control over verbosity.
|
|
||||||
*/
|
|
||||||
namespace Logger {
|
|
||||||
|
|
||||||
#ifdef _DEBUG
|
|
||||||
constexpr bool TRACE_ENABLED = true;
|
|
||||||
#else
|
|
||||||
constexpr bool TRACE_ENABLED = false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Log a trace message (only in debug builds)
|
|
||||||
*/
|
|
||||||
template<typename... Args>
|
|
||||||
inline void trace(const char* fmt, Args... args) {
|
|
||||||
if constexpr (TRACE_ENABLED) {
|
|
||||||
SDL_LogTrace(SDL_LOG_CATEGORY_APPLICATION, fmt, args...);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Log a debug message
|
|
||||||
*/
|
|
||||||
template<typename... Args>
|
|
||||||
inline void debug(const char* fmt, Args... args) {
|
|
||||||
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, fmt, args...);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Log an info message
|
|
||||||
*/
|
|
||||||
template<typename... Args>
|
|
||||||
inline void info(const char* fmt, Args... args) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, fmt, args...);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Log a warning message
|
|
||||||
*/
|
|
||||||
template<typename... Args>
|
|
||||||
inline void warn(const char* fmt, Args... args) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, fmt, args...);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Log an error message
|
|
||||||
*/
|
|
||||||
template<typename... Args>
|
|
||||||
inline void error(const char* fmt, Args... args) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, fmt, args...);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Logger
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage in MenuState.cpp:**
|
|
||||||
```cpp
|
|
||||||
#include "../utils/Logger.h"
|
|
||||||
|
|
||||||
void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
|
|
||||||
Logger::trace("MenuState::render entry");
|
|
||||||
|
|
||||||
// ... rest of render code
|
|
||||||
|
|
||||||
Logger::trace("MenuState::render exit");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 3: Update All Files
|
|
||||||
|
|
||||||
**Files to update:**
|
|
||||||
- `src/states/MenuState.cpp` (multiple locations)
|
|
||||||
- `src/main.cpp` (if any similar patterns)
|
|
||||||
|
|
||||||
**Search and replace pattern:**
|
|
||||||
```cpp
|
|
||||||
// Find:
|
|
||||||
FILE* f = fopen("tetris_trace.log", "a");
|
|
||||||
if (f) {
|
|
||||||
fprintf(f, ".*");
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace with:
|
|
||||||
Logger::trace("...");
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Improvement #3: Extract Common Patterns
|
|
||||||
|
|
||||||
### Step 1: Create ExitPopupHelper
|
|
||||||
|
|
||||||
**File:** `src/states/StateHelpers.h` (new file)
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file StateHelpers.h
|
|
||||||
* @brief Helper classes for common state patterns
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Helper for managing exit confirmation popup
|
|
||||||
*
|
|
||||||
* Encapsulates the common pattern of showing/hiding an exit popup
|
|
||||||
* and managing the selected button state.
|
|
||||||
*
|
|
||||||
* Example usage:
|
|
||||||
* @code
|
|
||||||
* ExitPopupHelper exitPopup(ctx.exitPopupSelectedButton, ctx.showExitConfirmPopup);
|
|
||||||
*
|
|
||||||
* if (exitPopup.isVisible()) {
|
|
||||||
* exitPopup.setSelection(0); // Select YES
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* if (exitPopup.isYesSelected()) {
|
|
||||||
* // Handle exit
|
|
||||||
* }
|
|
||||||
* @endcode
|
|
||||||
*/
|
|
||||||
class ExitPopupHelper {
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* @brief Construct helper with pointers to state variables
|
|
||||||
* @param selectedButton Pointer to selected button index (0=YES, 1=NO)
|
|
||||||
* @param showPopup Pointer to popup visibility flag
|
|
||||||
*/
|
|
||||||
ExitPopupHelper(int* selectedButton, bool* showPopup)
|
|
||||||
: m_selectedButton(selectedButton)
|
|
||||||
, m_showPopup(showPopup)
|
|
||||||
{}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set the selected button
|
|
||||||
* @param value 0 for YES, 1 for NO
|
|
||||||
*/
|
|
||||||
void setSelection(int value) {
|
|
||||||
if (m_selectedButton) {
|
|
||||||
*m_selectedButton = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the currently selected button
|
|
||||||
* @return 0 for YES, 1 for NO, defaults to 1 (NO) if pointer is null
|
|
||||||
*/
|
|
||||||
int getSelection() const {
|
|
||||||
return m_selectedButton ? *m_selectedButton : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Select YES button
|
|
||||||
*/
|
|
||||||
void selectYes() {
|
|
||||||
setSelection(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Select NO button
|
|
||||||
*/
|
|
||||||
void selectNo() {
|
|
||||||
setSelection(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if YES is selected
|
|
||||||
*/
|
|
||||||
bool isYesSelected() const {
|
|
||||||
return getSelection() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if NO is selected
|
|
||||||
*/
|
|
||||||
bool isNoSelected() const {
|
|
||||||
return getSelection() == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show the popup
|
|
||||||
*/
|
|
||||||
void show() {
|
|
||||||
if (m_showPopup) {
|
|
||||||
*m_showPopup = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Hide the popup
|
|
||||||
*/
|
|
||||||
void hide() {
|
|
||||||
if (m_showPopup) {
|
|
||||||
*m_showPopup = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if popup is visible
|
|
||||||
*/
|
|
||||||
bool isVisible() const {
|
|
||||||
return m_showPopup && *m_showPopup;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Toggle between YES and NO
|
|
||||||
*/
|
|
||||||
void toggleSelection() {
|
|
||||||
setSelection(isYesSelected() ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
int* m_selectedButton;
|
|
||||||
bool* m_showPopup;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 2: Update MenuState.cpp
|
|
||||||
|
|
||||||
**File:** `src/states/MenuState.cpp`
|
|
||||||
|
|
||||||
**Add include:**
|
|
||||||
```cpp
|
|
||||||
#include "StateHelpers.h"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```cpp
|
|
||||||
void MenuState::handleEvent(const SDL_Event& e) {
|
|
||||||
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
|
||||||
auto setExitSelection = [&](int value) {
|
|
||||||
if (ctx.exitPopupSelectedButton) {
|
|
||||||
*ctx.exitPopupSelectedButton = value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
auto getExitSelection = [&]() -> int {
|
|
||||||
return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
|
|
||||||
};
|
|
||||||
auto isExitPromptVisible = [&]() -> bool {
|
|
||||||
return ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup;
|
|
||||||
};
|
|
||||||
auto setExitPrompt = [&](bool visible) {
|
|
||||||
if (ctx.showExitConfirmPopup) {
|
|
||||||
*ctx.showExitConfirmPopup = visible;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isExitPromptVisible()) {
|
|
||||||
switch (e.key.scancode) {
|
|
||||||
case SDL_SCANCODE_LEFT:
|
|
||||||
case SDL_SCANCODE_UP:
|
|
||||||
setExitSelection(0);
|
|
||||||
return;
|
|
||||||
case SDL_SCANCODE_RIGHT:
|
|
||||||
case SDL_SCANCODE_DOWN:
|
|
||||||
setExitSelection(1);
|
|
||||||
return;
|
|
||||||
case SDL_SCANCODE_RETURN:
|
|
||||||
case SDL_SCANCODE_KP_ENTER:
|
|
||||||
case SDL_SCANCODE_SPACE:
|
|
||||||
if (getExitSelection() == 0) {
|
|
||||||
setExitPrompt(false);
|
|
||||||
if (ctx.requestQuit) {
|
|
||||||
ctx.requestQuit();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setExitPrompt(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case SDL_SCANCODE_ESCAPE:
|
|
||||||
setExitPrompt(false);
|
|
||||||
setExitSelection(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... rest of code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```cpp
|
|
||||||
void MenuState::handleEvent(const SDL_Event& e) {
|
|
||||||
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
|
||||||
ExitPopupHelper exitPopup(ctx.exitPopupSelectedButton, ctx.showExitConfirmPopup);
|
|
||||||
|
|
||||||
auto triggerPlay = [&]() {
|
|
||||||
if (ctx.startPlayTransition) {
|
|
||||||
ctx.startPlayTransition();
|
|
||||||
} else if (ctx.stateManager) {
|
|
||||||
ctx.stateManager->setState(AppState::Playing);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (exitPopup.isVisible()) {
|
|
||||||
switch (e.key.scancode) {
|
|
||||||
case SDL_SCANCODE_LEFT:
|
|
||||||
case SDL_SCANCODE_UP:
|
|
||||||
exitPopup.selectYes();
|
|
||||||
return;
|
|
||||||
case SDL_SCANCODE_RIGHT:
|
|
||||||
case SDL_SCANCODE_DOWN:
|
|
||||||
exitPopup.selectNo();
|
|
||||||
return;
|
|
||||||
case SDL_SCANCODE_RETURN:
|
|
||||||
case SDL_SCANCODE_KP_ENTER:
|
|
||||||
case SDL_SCANCODE_SPACE:
|
|
||||||
if (exitPopup.isYesSelected()) {
|
|
||||||
exitPopup.hide();
|
|
||||||
if (ctx.requestQuit) {
|
|
||||||
ctx.requestQuit();
|
|
||||||
} else {
|
|
||||||
SDL_Event quit{};
|
|
||||||
quit.type = SDL_EVENT_QUIT;
|
|
||||||
SDL_PushEvent(&quit);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
exitPopup.hide();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case SDL_SCANCODE_ESCAPE:
|
|
||||||
exitPopup.hide();
|
|
||||||
exitPopup.selectNo();
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (e.key.scancode) {
|
|
||||||
case SDL_SCANCODE_LEFT:
|
|
||||||
case SDL_SCANCODE_UP:
|
|
||||||
{
|
|
||||||
const int total = 4;
|
|
||||||
selectedButton = (selectedButton + total - 1) % total;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SDL_SCANCODE_RIGHT:
|
|
||||||
case SDL_SCANCODE_DOWN:
|
|
||||||
{
|
|
||||||
const int total = 4;
|
|
||||||
selectedButton = (selectedButton + 1) % total;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SDL_SCANCODE_RETURN:
|
|
||||||
case SDL_SCANCODE_KP_ENTER:
|
|
||||||
case SDL_SCANCODE_SPACE:
|
|
||||||
if (!ctx.stateManager) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (selectedButton) {
|
|
||||||
case 0:
|
|
||||||
triggerPlay();
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
if (ctx.requestFadeTransition) {
|
|
||||||
ctx.requestFadeTransition(AppState::LevelSelector);
|
|
||||||
} else if (ctx.stateManager) {
|
|
||||||
ctx.stateManager->setState(AppState::LevelSelector);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
if (ctx.requestFadeTransition) {
|
|
||||||
ctx.requestFadeTransition(AppState::Options);
|
|
||||||
} else if (ctx.stateManager) {
|
|
||||||
ctx.stateManager->setState(AppState::Options);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
exitPopup.show();
|
|
||||||
exitPopup.selectNo();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_ESCAPE:
|
|
||||||
exitPopup.show();
|
|
||||||
exitPopup.selectNo();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 3: Apply to Other States
|
|
||||||
|
|
||||||
Apply the same pattern to:
|
|
||||||
- `src/states/PlayingState.cpp`
|
|
||||||
- `src/states/OptionsState.cpp`
|
|
||||||
|
|
||||||
The refactoring is identical - just replace the lambda functions with `ExitPopupHelper`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Testing Your Changes
|
|
||||||
|
|
||||||
After implementing these improvements:
|
|
||||||
|
|
||||||
1. **Build the project:**
|
|
||||||
```powershell
|
|
||||||
cd d:\Sites\Work\tetris
|
|
||||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
|
|
||||||
cmake --build build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Run the game:**
|
|
||||||
```powershell
|
|
||||||
.\build\Debug\tetris.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Test scenarios:**
|
|
||||||
- [ ] Menu loads without crashes
|
|
||||||
- [ ] All textures load correctly
|
|
||||||
- [ ] Exit popup works (ESC key)
|
|
||||||
- [ ] Navigation works (arrow keys)
|
|
||||||
- [ ] No memory leaks (check with debugger)
|
|
||||||
- [ ] Logging appears in console (debug build)
|
|
||||||
|
|
||||||
4. **Check for memory leaks:**
|
|
||||||
- Run with Visual Studio debugger
|
|
||||||
- Check Output window for memory leak reports
|
|
||||||
- Should see no leaks from SDL textures
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Expected Impact
|
|
||||||
|
|
||||||
After implementing these three improvements:
|
|
||||||
|
|
||||||
| Metric | Before | After | Improvement |
|
|
||||||
|--------|--------|-------|-------------|
|
|
||||||
| **Memory Safety** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +67% |
|
|
||||||
| **Code Clarity** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +25% |
|
|
||||||
| **Maintainability** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +25% |
|
|
||||||
| **Lines of Code** | 100% | ~95% | -5% |
|
|
||||||
| **Potential Bugs** | Medium | Low | -50% |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Next Steps
|
|
||||||
|
|
||||||
After successfully implementing these improvements:
|
|
||||||
|
|
||||||
1. Review the full `CODE_ANALYSIS.md` for more recommendations
|
|
||||||
2. Check `IMPROVEMENTS_CHECKLIST.md` for the complete task list
|
|
||||||
3. Consider implementing the medium-priority items next
|
|
||||||
4. Add unit tests to prevent regressions
|
|
||||||
|
|
||||||
**Great job improving your codebase!** 🚀
|
|
||||||
BIN
assets/images/asteroids_001.png
Normal file
BIN
assets/images/asteroids_001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
Binary file not shown.
Binary file not shown.
BIN
assets/music/asteroid-destroy.mp3
Normal file
BIN
assets/music/asteroid-destroy.mp3
Normal file
Binary file not shown.
@ -1,5 +0,0 @@
|
|||||||
#include <SDL3/SDL.h>
|
|
||||||
#include <iostream>
|
|
||||||
int main() { std::cout << \
|
|
||||||
SDL_EVENT_QUIT:
|
|
||||||
\ << SDL_EVENT_QUIT << std::endl; return 0; }
|
|
||||||
@ -82,6 +82,75 @@ static const std::array<SDL_Color, PIECE_COUNT + 1> COLORS = {{
|
|||||||
SDL_Color{255, 160, 0, 255}, // L
|
SDL_Color{255, 160, 0, 255}, // L
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
static std::string GetLevelStoryText(int level) {
|
||||||
|
int lvl = std::clamp(level, 1, 100);
|
||||||
|
|
||||||
|
// Milestones
|
||||||
|
switch (lvl) {
|
||||||
|
case 1: return "Launch log: training run, light debris ahead.";
|
||||||
|
case 25: return "Checkpoint: dense field reported, shields ready.";
|
||||||
|
case 50: return "Midway brief: hull stress rising, stay sharp.";
|
||||||
|
case 75: return "Emergency corridor: comms unstable, proceed blind.";
|
||||||
|
case 100: return "Final anomaly: unknown mass ahead, hold course.";
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Pool { int minL, maxL; std::vector<std::string> lines; };
|
||||||
|
static const std::vector<Pool> pools = {
|
||||||
|
{1, 10, {
|
||||||
|
"Departure logged: light debris, stay on vector.",
|
||||||
|
"Training sector: minimal drift, keep sensors warm.",
|
||||||
|
"Calm approach: verify thrusters and nav locks.",
|
||||||
|
"Outer ring dust: watch for slow movers.",
|
||||||
|
"Clear lanes ahead: focus on smooth rotations."
|
||||||
|
}},
|
||||||
|
{11, 25, {
|
||||||
|
"Asteroid belt thickening; micro-impacts likely.",
|
||||||
|
"Density rising: plot short burns only.",
|
||||||
|
"Field report: medium fragments, unpredictable spin.",
|
||||||
|
"Warning: overlapping paths, reduce horizontal drift.",
|
||||||
|
"Rock chorus ahead; keep payload stable."
|
||||||
|
}},
|
||||||
|
{26, 40, {
|
||||||
|
"Unstable sector: abandoned relays drifting erratic.",
|
||||||
|
"Salvage echoes detected; debris wakes may tug.",
|
||||||
|
"Hull groans recorded; inert structures nearby.",
|
||||||
|
"Navigation buoys dark; trust instruments only.",
|
||||||
|
"Magnetic static rising; expect odd rotations."
|
||||||
|
}},
|
||||||
|
{41, 60, {
|
||||||
|
"Core corridor: heavy asteroids, minimal clearance.",
|
||||||
|
"Impact risk high: armor checks recommended.",
|
||||||
|
"Dense stone flow; time burns carefully.",
|
||||||
|
"Grav eddies noted; blocks may drift late.",
|
||||||
|
"Core shards are brittle; expect sudden splits."
|
||||||
|
}},
|
||||||
|
{61, 80, {
|
||||||
|
"Critical zone: alarms pinned, route unstable.",
|
||||||
|
"Emergency pattern: glide, then cut thrust.",
|
||||||
|
"Sensors flare; debris ionized, visibility low.",
|
||||||
|
"Thermals spiking; keep pieces tight and fast.",
|
||||||
|
"Silent channel; assume worst-case collision."
|
||||||
|
}},
|
||||||
|
{81, 100, {
|
||||||
|
"Unknown space: signals warp, gravity unreliable.",
|
||||||
|
"Anomaly bloom ahead; shapes flicker unpredictably.",
|
||||||
|
"Final drift: void sings through hull plates.",
|
||||||
|
"Black sector: map useless, fly by instinct.",
|
||||||
|
"Edge of chart: nothing responds, just move."
|
||||||
|
}}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& pool : pools) {
|
||||||
|
if (lvl >= pool.minL && lvl <= pool.maxL && !pool.lines.empty()) {
|
||||||
|
size_t idx = static_cast<size_t>((lvl - pool.minL) % pool.lines.size());
|
||||||
|
return pool.lines[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Mission log update unavailable.";
|
||||||
|
}
|
||||||
|
|
||||||
struct TetrisApp::Impl {
|
struct TetrisApp::Impl {
|
||||||
// Global collector for asset loading errors shown on the loading screen
|
// Global collector for asset loading errors shown on the loading screen
|
||||||
std::vector<std::string> assetLoadErrors;
|
std::vector<std::string> assetLoadErrors;
|
||||||
@ -137,6 +206,7 @@ struct TetrisApp::Impl {
|
|||||||
int mainScreenH = 0;
|
int mainScreenH = 0;
|
||||||
|
|
||||||
SDL_Texture* blocksTex = nullptr;
|
SDL_Texture* blocksTex = nullptr;
|
||||||
|
SDL_Texture* asteroidsTex = nullptr;
|
||||||
SDL_Texture* scorePanelTex = nullptr;
|
SDL_Texture* scorePanelTex = nullptr;
|
||||||
SDL_Texture* statisticsPanelTex = nullptr;
|
SDL_Texture* statisticsPanelTex = nullptr;
|
||||||
SDL_Texture* nextPanelTex = nullptr;
|
SDL_Texture* nextPanelTex = nullptr;
|
||||||
@ -163,6 +233,7 @@ struct TetrisApp::Impl {
|
|||||||
std::vector<std::string> tripleSounds;
|
std::vector<std::string> tripleSounds;
|
||||||
std::vector<std::string> tetrisSounds;
|
std::vector<std::string> tetrisSounds;
|
||||||
bool suppressLineVoiceForLevelUp = false;
|
bool suppressLineVoiceForLevelUp = false;
|
||||||
|
bool skipNextLevelUpJingle = false;
|
||||||
|
|
||||||
AppState state = AppState::Loading;
|
AppState state = AppState::Loading;
|
||||||
double loadingProgress = 0.0;
|
double loadingProgress = 0.0;
|
||||||
@ -184,13 +255,32 @@ struct TetrisApp::Impl {
|
|||||||
float menuFadeAlpha = 0.0f;
|
float menuFadeAlpha = 0.0f;
|
||||||
double MENU_PLAY_FADE_DURATION_MS = 450.0;
|
double MENU_PLAY_FADE_DURATION_MS = 450.0;
|
||||||
AppState menuFadeTarget = AppState::Menu;
|
AppState menuFadeTarget = AppState::Menu;
|
||||||
|
|
||||||
|
enum class CountdownSource { MenuStart, ChallengeLevel };
|
||||||
bool menuPlayCountdownArmed = false;
|
bool menuPlayCountdownArmed = false;
|
||||||
bool gameplayCountdownActive = false;
|
bool gameplayCountdownActive = false;
|
||||||
double gameplayCountdownElapsed = 0.0;
|
double gameplayCountdownElapsed = 0.0;
|
||||||
int gameplayCountdownIndex = 0;
|
int gameplayCountdownIndex = 0;
|
||||||
double GAMEPLAY_COUNTDOWN_STEP_MS = 400.0;
|
double GAMEPLAY_COUNTDOWN_STEP_MS = 400.0;
|
||||||
std::array<const char*, 4> GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" };
|
std::array<const char*, 4> GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" };
|
||||||
|
CountdownSource gameplayCountdownSource = CountdownSource::MenuStart;
|
||||||
|
int countdownLevel = 0;
|
||||||
|
int countdownGoalAsteroids = 0;
|
||||||
|
bool countdownAdvancesChallenge = false;
|
||||||
|
bool challengeCountdownWaitingForSpace = false;
|
||||||
double gameplayBackgroundClockMs = 0.0;
|
double gameplayBackgroundClockMs = 0.0;
|
||||||
|
std::string challengeStoryText;
|
||||||
|
int challengeStoryLevel = 0;
|
||||||
|
float challengeStoryAlpha = 0.0f;
|
||||||
|
double challengeStoryClockMs = 0.0;
|
||||||
|
|
||||||
|
// Challenge clear FX (celebratory board explosion before countdown)
|
||||||
|
bool challengeClearFxActive = false;
|
||||||
|
double challengeClearFxElapsedMs = 0.0;
|
||||||
|
double challengeClearFxDurationMs = 0.0;
|
||||||
|
int challengeClearFxNextLevel = 0;
|
||||||
|
std::vector<int> challengeClearFxOrder;
|
||||||
|
std::mt19937 challengeClearFxRng{std::random_device{}()};
|
||||||
|
|
||||||
std::unique_ptr<StateManager> stateMgr;
|
std::unique_ptr<StateManager> stateMgr;
|
||||||
StateContext ctx{};
|
StateContext ctx{};
|
||||||
@ -369,11 +459,19 @@ int TetrisApp::Impl::init()
|
|||||||
});
|
});
|
||||||
|
|
||||||
game->setLevelUpCallback([this](int /*newLevel*/) {
|
game->setLevelUpCallback([this](int /*newLevel*/) {
|
||||||
|
if (skipNextLevelUpJingle) {
|
||||||
|
skipNextLevelUpJingle = false;
|
||||||
|
} else {
|
||||||
SoundEffectManager::instance().playSound("new_level", 1.0f);
|
SoundEffectManager::instance().playSound("new_level", 1.0f);
|
||||||
SoundEffectManager::instance().playSound("lets_go", 1.0f);
|
SoundEffectManager::instance().playSound("lets_go", 1.0f);
|
||||||
|
}
|
||||||
suppressLineVoiceForLevelUp = true;
|
suppressLineVoiceForLevelUp = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
game->setAsteroidDestroyedCallback([](AsteroidType /*type*/) {
|
||||||
|
SoundEffectManager::instance().playSound("asteroid_destroy", 0.9f);
|
||||||
|
});
|
||||||
|
|
||||||
state = AppState::Loading;
|
state = AppState::Loading;
|
||||||
loadingProgress = 0.0;
|
loadingProgress = 0.0;
|
||||||
loadStart = SDL_GetTicks();
|
loadStart = SDL_GetTicks();
|
||||||
@ -419,6 +517,7 @@ int TetrisApp::Impl::init()
|
|||||||
ctx.logoSmallW = logoSmallW;
|
ctx.logoSmallW = logoSmallW;
|
||||||
ctx.logoSmallH = logoSmallH;
|
ctx.logoSmallH = logoSmallH;
|
||||||
ctx.backgroundTex = nullptr;
|
ctx.backgroundTex = nullptr;
|
||||||
|
ctx.asteroidsTex = asteroidsTex;
|
||||||
ctx.blocksTex = blocksTex;
|
ctx.blocksTex = blocksTex;
|
||||||
ctx.scorePanelTex = scorePanelTex;
|
ctx.scorePanelTex = scorePanelTex;
|
||||||
ctx.statisticsPanelTex = statisticsPanelTex;
|
ctx.statisticsPanelTex = statisticsPanelTex;
|
||||||
@ -435,6 +534,14 @@ int TetrisApp::Impl::init()
|
|||||||
ctx.exitPopupSelectedButton = &exitPopupSelectedButton;
|
ctx.exitPopupSelectedButton = &exitPopupSelectedButton;
|
||||||
ctx.gameplayCountdownActive = &gameplayCountdownActive;
|
ctx.gameplayCountdownActive = &gameplayCountdownActive;
|
||||||
ctx.menuPlayCountdownArmed = &menuPlayCountdownArmed;
|
ctx.menuPlayCountdownArmed = &menuPlayCountdownArmed;
|
||||||
|
ctx.skipNextLevelUpJingle = &skipNextLevelUpJingle;
|
||||||
|
ctx.challengeClearFxActive = &challengeClearFxActive;
|
||||||
|
ctx.challengeClearFxElapsedMs = &challengeClearFxElapsedMs;
|
||||||
|
ctx.challengeClearFxDurationMs = &challengeClearFxDurationMs;
|
||||||
|
ctx.challengeClearFxOrder = &challengeClearFxOrder;
|
||||||
|
ctx.challengeStoryText = &challengeStoryText;
|
||||||
|
ctx.challengeStoryLevel = &challengeStoryLevel;
|
||||||
|
ctx.challengeStoryAlpha = &challengeStoryAlpha;
|
||||||
ctx.playerName = &playerName;
|
ctx.playerName = &playerName;
|
||||||
ctx.fullscreenFlag = &isFullscreen;
|
ctx.fullscreenFlag = &isFullscreen;
|
||||||
ctx.applyFullscreen = [this](bool enable) {
|
ctx.applyFullscreen = [this](bool enable) {
|
||||||
@ -555,6 +662,52 @@ void TetrisApp::Impl::runLoop()
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto captureChallengeStory = [this](int level) {
|
||||||
|
int lvl = std::clamp(level, 1, 100);
|
||||||
|
challengeStoryLevel = lvl;
|
||||||
|
challengeStoryText = GetLevelStoryText(lvl);
|
||||||
|
challengeStoryClockMs = 0.0;
|
||||||
|
challengeStoryAlpha = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto startChallengeClearFx = [this](int nextLevel) {
|
||||||
|
challengeClearFxOrder.clear();
|
||||||
|
const auto& boardRef = game->boardRef();
|
||||||
|
const auto& asteroidRef = game->asteroidCells();
|
||||||
|
for (int idx = 0; idx < Game::COLS * Game::ROWS; ++idx) {
|
||||||
|
if (boardRef[idx] != 0 || asteroidRef[idx].has_value()) {
|
||||||
|
challengeClearFxOrder.push_back(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (challengeClearFxOrder.empty()) {
|
||||||
|
challengeClearFxOrder.reserve(Game::COLS * Game::ROWS);
|
||||||
|
for (int idx = 0; idx < Game::COLS * Game::ROWS; ++idx) {
|
||||||
|
challengeClearFxOrder.push_back(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Seed FX RNG deterministically from the game's challenge seed so animations
|
||||||
|
// are reproducible per-run and per-level. Fall back to a random seed if game absent.
|
||||||
|
if (game) {
|
||||||
|
challengeClearFxRng.seed(game->getChallengeSeedBase() + static_cast<uint32_t>(nextLevel));
|
||||||
|
} else {
|
||||||
|
challengeClearFxRng.seed(std::random_device{}());
|
||||||
|
}
|
||||||
|
std::shuffle(challengeClearFxOrder.begin(), challengeClearFxOrder.end(), challengeClearFxRng);
|
||||||
|
|
||||||
|
challengeClearFxElapsedMs = 0.0;
|
||||||
|
challengeClearFxDurationMs = std::clamp(800.0 + static_cast<double>(challengeClearFxOrder.size()) * 8.0, 900.0, 2600.0);
|
||||||
|
challengeClearFxNextLevel = nextLevel;
|
||||||
|
challengeClearFxActive = true;
|
||||||
|
gameplayCountdownActive = false;
|
||||||
|
gameplayCountdownElapsed = 0.0;
|
||||||
|
gameplayCountdownIndex = 0;
|
||||||
|
menuPlayCountdownArmed = false;
|
||||||
|
if (game) {
|
||||||
|
game->setPaused(true);
|
||||||
|
}
|
||||||
|
SoundEffectManager::instance().playSound("challenge_clear", 0.8f);
|
||||||
|
};
|
||||||
|
|
||||||
while (running)
|
while (running)
|
||||||
{
|
{
|
||||||
if (!ctx.scores && scoresLoadComplete.load(std::memory_order_acquire)) {
|
if (!ctx.scores && scoresLoadComplete.load(std::memory_order_acquire)) {
|
||||||
@ -703,7 +856,12 @@ void TetrisApp::Impl::runLoop()
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) {
|
if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) {
|
||||||
|
if (game->getMode() == GameMode::Challenge) {
|
||||||
|
game->startChallengeRun(1);
|
||||||
|
} else {
|
||||||
|
game->setMode(GameMode::Endless);
|
||||||
game->reset(startLevelSelection);
|
game->reset(startLevelSelection);
|
||||||
|
}
|
||||||
state = AppState::Playing;
|
state = AppState::Playing;
|
||||||
stateMgr->setState(state);
|
stateMgr->setState(state);
|
||||||
} else if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
|
} else if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||||
@ -732,6 +890,16 @@ void TetrisApp::Impl::runLoop()
|
|||||||
if (menuInput.activated) {
|
if (menuInput.activated) {
|
||||||
switch (*menuInput.activated) {
|
switch (*menuInput.activated) {
|
||||||
case ui::BottomMenuItem::Play:
|
case ui::BottomMenuItem::Play:
|
||||||
|
if (game) game->setMode(GameMode::Endless);
|
||||||
|
startMenuPlayTransition();
|
||||||
|
break;
|
||||||
|
case ui::BottomMenuItem::Challenge:
|
||||||
|
if (game) {
|
||||||
|
game->setMode(GameMode::Challenge);
|
||||||
|
// Suppress the initial level-up jingle when starting Challenge from menu
|
||||||
|
skipNextLevelUpJingle = true;
|
||||||
|
game->startChallengeRun(1);
|
||||||
|
}
|
||||||
startMenuPlayTransition();
|
startMenuPlayTransition();
|
||||||
break;
|
break;
|
||||||
case ui::BottomMenuItem::Level:
|
case ui::BottomMenuItem::Level:
|
||||||
@ -841,6 +1009,28 @@ void TetrisApp::Impl::runLoop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
||||||
|
if (gameplayCountdownActive && gameplayCountdownSource == CountdownSource::ChallengeLevel && challengeCountdownWaitingForSpace) {
|
||||||
|
if (e.key.scancode == SDL_SCANCODE_SPACE) {
|
||||||
|
challengeCountdownWaitingForSpace = false;
|
||||||
|
gameplayCountdownElapsed = 0.0;
|
||||||
|
gameplayCountdownIndex = 0;
|
||||||
|
} else if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||||
|
// Show quit popup, keep game paused, cancel countdown
|
||||||
|
if (!showExitConfirmPopup) {
|
||||||
|
showExitConfirmPopup = true;
|
||||||
|
exitPopupSelectedButton = 1; // default to NO
|
||||||
|
}
|
||||||
|
gameplayCountdownActive = false;
|
||||||
|
menuPlayCountdownArmed = false;
|
||||||
|
gameplayCountdownElapsed = 0.0;
|
||||||
|
gameplayCountdownIndex = 0;
|
||||||
|
countdownAdvancesChallenge = false;
|
||||||
|
challengeCountdownWaitingForSpace = false;
|
||||||
|
if (game) game->setPaused(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -850,6 +1040,69 @@ void TetrisApp::Impl::runLoop()
|
|||||||
if (frameMs > 100.0) frameMs = 100.0;
|
if (frameMs > 100.0) frameMs = 100.0;
|
||||||
gameplayBackgroundClockMs += frameMs;
|
gameplayBackgroundClockMs += frameMs;
|
||||||
|
|
||||||
|
auto clearChallengeStory = [this]() {
|
||||||
|
challengeStoryText.clear();
|
||||||
|
challengeStoryLevel = 0;
|
||||||
|
challengeStoryAlpha = 0.0f;
|
||||||
|
challengeStoryClockMs = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update challenge story fade/timeout; during countdown wait we keep it fully visible
|
||||||
|
if (state == AppState::Playing && game && game->getMode() == GameMode::Challenge && !challengeStoryText.empty()) {
|
||||||
|
if (gameplayCountdownActive && gameplayCountdownSource == CountdownSource::ChallengeLevel && challengeCountdownWaitingForSpace) {
|
||||||
|
// Locked-visible while waiting
|
||||||
|
challengeStoryAlpha = 1.0f;
|
||||||
|
} else {
|
||||||
|
const double fadeInMs = 320.0;
|
||||||
|
const double holdMs = 3200.0;
|
||||||
|
const double fadeOutMs = 900.0;
|
||||||
|
const double totalMs = fadeInMs + holdMs + fadeOutMs;
|
||||||
|
challengeStoryClockMs += frameMs;
|
||||||
|
if (challengeStoryClockMs >= totalMs) {
|
||||||
|
clearChallengeStory();
|
||||||
|
} else {
|
||||||
|
double a = 1.0;
|
||||||
|
if (challengeStoryClockMs < fadeInMs) {
|
||||||
|
a = challengeStoryClockMs / fadeInMs;
|
||||||
|
} else if (challengeStoryClockMs > fadeInMs + holdMs) {
|
||||||
|
double t = challengeStoryClockMs - (fadeInMs + holdMs);
|
||||||
|
a = std::max(0.0, 1.0 - t / fadeOutMs);
|
||||||
|
}
|
||||||
|
challengeStoryAlpha = static_cast<float>(std::clamp(a, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearChallengeStory();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (challengeClearFxActive) {
|
||||||
|
challengeClearFxElapsedMs += frameMs;
|
||||||
|
if (challengeClearFxElapsedMs >= challengeClearFxDurationMs) {
|
||||||
|
challengeClearFxElapsedMs = challengeClearFxDurationMs;
|
||||||
|
challengeClearFxActive = false;
|
||||||
|
if (challengeClearFxNextLevel > 0) {
|
||||||
|
// Advance to the next challenge level immediately so the countdown shows the new board/asteroids
|
||||||
|
if (game) {
|
||||||
|
game->beginNextChallengeLevel();
|
||||||
|
game->setPaused(true);
|
||||||
|
}
|
||||||
|
gameplayCountdownSource = CountdownSource::ChallengeLevel;
|
||||||
|
countdownLevel = challengeClearFxNextLevel;
|
||||||
|
countdownGoalAsteroids = challengeClearFxNextLevel;
|
||||||
|
captureChallengeStory(countdownLevel);
|
||||||
|
countdownAdvancesChallenge = false; // already advanced
|
||||||
|
gameplayCountdownActive = true;
|
||||||
|
challengeCountdownWaitingForSpace = true;
|
||||||
|
menuPlayCountdownArmed = false;
|
||||||
|
gameplayCountdownElapsed = 0.0;
|
||||||
|
gameplayCountdownIndex = 0;
|
||||||
|
SoundEffectManager::instance().playSound("new_level", 1.0f);
|
||||||
|
skipNextLevelUpJingle = true;
|
||||||
|
}
|
||||||
|
challengeClearFxNextLevel = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const bool *ks = SDL_GetKeyboardState(nullptr);
|
const bool *ks = SDL_GetKeyboardState(nullptr);
|
||||||
bool left = state == AppState::Playing && ks[SDL_SCANCODE_LEFT];
|
bool left = state == AppState::Playing && ks[SDL_SCANCODE_LEFT];
|
||||||
bool right = state == AppState::Playing && ks[SDL_SCANCODE_RIGHT];
|
bool right = state == AppState::Playing && ks[SDL_SCANCODE_RIGHT];
|
||||||
@ -976,7 +1229,8 @@ void TetrisApp::Impl::runLoop()
|
|||||||
Assets::PANEL_SCORE,
|
Assets::PANEL_SCORE,
|
||||||
Assets::PANEL_STATS,
|
Assets::PANEL_STATS,
|
||||||
Assets::NEXT_PANEL,
|
Assets::NEXT_PANEL,
|
||||||
Assets::HOLD_PANEL
|
Assets::HOLD_PANEL,
|
||||||
|
Assets::ASTEROID_SPRITE
|
||||||
};
|
};
|
||||||
for (auto &p : queuedPaths) {
|
for (auto &p : queuedPaths) {
|
||||||
loadingManager->queueTexture(p);
|
loadingManager->queueTexture(p);
|
||||||
@ -986,9 +1240,15 @@ void TetrisApp::Impl::runLoop()
|
|||||||
SoundEffectManager::instance().init();
|
SoundEffectManager::instance().init();
|
||||||
loadedTasks.fetch_add(1);
|
loadedTasks.fetch_add(1);
|
||||||
|
|
||||||
const std::vector<std::string> audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level"};
|
const std::vector<std::string> audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level","asteroid_destroy","challenge_clear"};
|
||||||
for (const auto &id : audioIds) {
|
for (const auto &id : audioIds) {
|
||||||
std::string basePath = "assets/music/" + (id == "hard_drop" ? "hard_drop_001" : id);
|
std::string basePath = "assets/music/" + (id == "hard_drop"
|
||||||
|
? "hard_drop_001"
|
||||||
|
: (id == "challenge_clear"
|
||||||
|
? "GONG0"
|
||||||
|
: (id == "asteroid_destroy"
|
||||||
|
? "asteroid-destroy"
|
||||||
|
: id)));
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lk(currentLoadingMutex);
|
std::lock_guard<std::mutex> lk(currentLoadingMutex);
|
||||||
currentLoadingFile = basePath;
|
currentLoadingFile = basePath;
|
||||||
@ -1011,6 +1271,7 @@ void TetrisApp::Impl::runLoop()
|
|||||||
logoSmallTex = assetLoader.getTexture(Assets::LOGO);
|
logoSmallTex = assetLoader.getTexture(Assets::LOGO);
|
||||||
mainScreenTex = assetLoader.getTexture(Assets::MAIN_SCREEN);
|
mainScreenTex = assetLoader.getTexture(Assets::MAIN_SCREEN);
|
||||||
blocksTex = assetLoader.getTexture(Assets::BLOCKS_SPRITE);
|
blocksTex = assetLoader.getTexture(Assets::BLOCKS_SPRITE);
|
||||||
|
asteroidsTex = assetLoader.getTexture(Assets::ASTEROID_SPRITE);
|
||||||
scorePanelTex = assetLoader.getTexture(Assets::PANEL_SCORE);
|
scorePanelTex = assetLoader.getTexture(Assets::PANEL_SCORE);
|
||||||
statisticsPanelTex = assetLoader.getTexture(Assets::PANEL_STATS);
|
statisticsPanelTex = assetLoader.getTexture(Assets::PANEL_STATS);
|
||||||
nextPanelTex = assetLoader.getTexture(Assets::NEXT_PANEL);
|
nextPanelTex = assetLoader.getTexture(Assets::NEXT_PANEL);
|
||||||
@ -1043,6 +1304,7 @@ void TetrisApp::Impl::runLoop()
|
|||||||
legacyLoad(Assets::LOGO, logoSmallTex, &logoSmallW, &logoSmallH);
|
legacyLoad(Assets::LOGO, logoSmallTex, &logoSmallW, &logoSmallH);
|
||||||
legacyLoad(Assets::MAIN_SCREEN, mainScreenTex, &mainScreenW, &mainScreenH);
|
legacyLoad(Assets::MAIN_SCREEN, mainScreenTex, &mainScreenW, &mainScreenH);
|
||||||
legacyLoad(Assets::BLOCKS_SPRITE, blocksTex);
|
legacyLoad(Assets::BLOCKS_SPRITE, blocksTex);
|
||||||
|
legacyLoad(Assets::ASTEROID_SPRITE, asteroidsTex);
|
||||||
legacyLoad(Assets::PANEL_SCORE, scorePanelTex);
|
legacyLoad(Assets::PANEL_SCORE, scorePanelTex);
|
||||||
legacyLoad(Assets::PANEL_STATS, statisticsPanelTex);
|
legacyLoad(Assets::PANEL_STATS, statisticsPanelTex);
|
||||||
legacyLoad(Assets::NEXT_PANEL, nextPanelTex);
|
legacyLoad(Assets::NEXT_PANEL, nextPanelTex);
|
||||||
@ -1195,12 +1457,20 @@ void TetrisApp::Impl::runLoop()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state == AppState::Playing && game && game->getMode() == GameMode::Challenge && !gameplayCountdownActive && !challengeClearFxActive) {
|
||||||
|
int queuedLevel = game->consumeQueuedChallengeLevel();
|
||||||
|
if (queuedLevel > 0) {
|
||||||
|
startChallengeClearFx(queuedLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.logoTex = logoTex;
|
ctx.logoTex = logoTex;
|
||||||
ctx.logoSmallTex = logoSmallTex;
|
ctx.logoSmallTex = logoSmallTex;
|
||||||
ctx.logoSmallW = logoSmallW;
|
ctx.logoSmallW = logoSmallW;
|
||||||
ctx.logoSmallH = logoSmallH;
|
ctx.logoSmallH = logoSmallH;
|
||||||
ctx.backgroundTex = backgroundTex;
|
ctx.backgroundTex = backgroundTex;
|
||||||
ctx.blocksTex = blocksTex;
|
ctx.blocksTex = blocksTex;
|
||||||
|
ctx.asteroidsTex = asteroidsTex;
|
||||||
ctx.scorePanelTex = scorePanelTex;
|
ctx.scorePanelTex = scorePanelTex;
|
||||||
ctx.statisticsPanelTex = statisticsPanelTex;
|
ctx.statisticsPanelTex = statisticsPanelTex;
|
||||||
ctx.nextPanelTex = nextPanelTex;
|
ctx.nextPanelTex = nextPanelTex;
|
||||||
@ -1219,6 +1489,20 @@ void TetrisApp::Impl::runLoop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (menuFadeTarget == AppState::Playing) {
|
if (menuFadeTarget == AppState::Playing) {
|
||||||
|
gameplayCountdownSource = (game && game->getMode() == GameMode::Challenge)
|
||||||
|
? CountdownSource::ChallengeLevel
|
||||||
|
: CountdownSource::MenuStart;
|
||||||
|
countdownLevel = game ? game->challengeLevel() : 1;
|
||||||
|
countdownGoalAsteroids = countdownLevel;
|
||||||
|
if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
|
||||||
|
captureChallengeStory(countdownLevel);
|
||||||
|
challengeCountdownWaitingForSpace = true;
|
||||||
|
} else {
|
||||||
|
challengeStoryText.clear();
|
||||||
|
challengeStoryLevel = 0;
|
||||||
|
challengeCountdownWaitingForSpace = false;
|
||||||
|
}
|
||||||
|
countdownAdvancesChallenge = false;
|
||||||
menuPlayCountdownArmed = true;
|
menuPlayCountdownArmed = true;
|
||||||
gameplayCountdownActive = false;
|
gameplayCountdownActive = false;
|
||||||
gameplayCountdownIndex = 0;
|
gameplayCountdownIndex = 0;
|
||||||
@ -1229,6 +1513,7 @@ void TetrisApp::Impl::runLoop()
|
|||||||
gameplayCountdownActive = false;
|
gameplayCountdownActive = false;
|
||||||
gameplayCountdownIndex = 0;
|
gameplayCountdownIndex = 0;
|
||||||
gameplayCountdownElapsed = 0.0;
|
gameplayCountdownElapsed = 0.0;
|
||||||
|
challengeCountdownWaitingForSpace = false;
|
||||||
game->setPaused(false);
|
game->setPaused(false);
|
||||||
}
|
}
|
||||||
menuFadePhase = MenuFadePhase::FadeIn;
|
menuFadePhase = MenuFadePhase::FadeIn;
|
||||||
@ -1246,6 +1531,20 @@ void TetrisApp::Impl::runLoop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (menuFadePhase == MenuFadePhase::None && menuPlayCountdownArmed && !gameplayCountdownActive && state == AppState::Playing) {
|
if (menuFadePhase == MenuFadePhase::None && menuPlayCountdownArmed && !gameplayCountdownActive && state == AppState::Playing) {
|
||||||
|
gameplayCountdownSource = (game && game->getMode() == GameMode::Challenge)
|
||||||
|
? CountdownSource::ChallengeLevel
|
||||||
|
: CountdownSource::MenuStart;
|
||||||
|
countdownLevel = game ? game->challengeLevel() : 1;
|
||||||
|
countdownGoalAsteroids = countdownLevel;
|
||||||
|
if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
|
||||||
|
captureChallengeStory(countdownLevel);
|
||||||
|
challengeCountdownWaitingForSpace = true;
|
||||||
|
} else {
|
||||||
|
challengeStoryText.clear();
|
||||||
|
challengeStoryLevel = 0;
|
||||||
|
challengeCountdownWaitingForSpace = false;
|
||||||
|
}
|
||||||
|
countdownAdvancesChallenge = false;
|
||||||
gameplayCountdownActive = true;
|
gameplayCountdownActive = true;
|
||||||
menuPlayCountdownArmed = false;
|
menuPlayCountdownArmed = false;
|
||||||
gameplayCountdownElapsed = 0.0;
|
gameplayCountdownElapsed = 0.0;
|
||||||
@ -1254,6 +1553,7 @@ void TetrisApp::Impl::runLoop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (gameplayCountdownActive && state == AppState::Playing) {
|
if (gameplayCountdownActive && state == AppState::Playing) {
|
||||||
|
if (!challengeCountdownWaitingForSpace || gameplayCountdownSource != CountdownSource::ChallengeLevel) {
|
||||||
gameplayCountdownElapsed += frameMs;
|
gameplayCountdownElapsed += frameMs;
|
||||||
if (gameplayCountdownElapsed >= GAMEPLAY_COUNTDOWN_STEP_MS) {
|
if (gameplayCountdownElapsed >= GAMEPLAY_COUNTDOWN_STEP_MS) {
|
||||||
gameplayCountdownElapsed -= GAMEPLAY_COUNTDOWN_STEP_MS;
|
gameplayCountdownElapsed -= GAMEPLAY_COUNTDOWN_STEP_MS;
|
||||||
@ -1262,19 +1562,34 @@ void TetrisApp::Impl::runLoop()
|
|||||||
gameplayCountdownActive = false;
|
gameplayCountdownActive = false;
|
||||||
gameplayCountdownElapsed = 0.0;
|
gameplayCountdownElapsed = 0.0;
|
||||||
gameplayCountdownIndex = 0;
|
gameplayCountdownIndex = 0;
|
||||||
|
if (gameplayCountdownSource == CountdownSource::ChallengeLevel && countdownAdvancesChallenge && game) {
|
||||||
|
game->beginNextChallengeLevel();
|
||||||
|
}
|
||||||
|
countdownAdvancesChallenge = false;
|
||||||
game->setPaused(false);
|
game->setPaused(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (state != AppState::Playing && gameplayCountdownActive) {
|
if (state != AppState::Playing && gameplayCountdownActive) {
|
||||||
gameplayCountdownActive = false;
|
gameplayCountdownActive = false;
|
||||||
menuPlayCountdownArmed = false;
|
menuPlayCountdownArmed = false;
|
||||||
gameplayCountdownElapsed = 0.0;
|
gameplayCountdownElapsed = 0.0;
|
||||||
gameplayCountdownIndex = 0;
|
gameplayCountdownIndex = 0;
|
||||||
|
countdownAdvancesChallenge = false;
|
||||||
|
challengeCountdownWaitingForSpace = false;
|
||||||
game->setPaused(false);
|
game->setPaused(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state != AppState::Playing && challengeClearFxActive) {
|
||||||
|
challengeClearFxActive = false;
|
||||||
|
challengeClearFxElapsedMs = 0.0;
|
||||||
|
challengeClearFxDurationMs = 0.0;
|
||||||
|
challengeClearFxNextLevel = 0;
|
||||||
|
challengeClearFxOrder.clear();
|
||||||
|
}
|
||||||
|
|
||||||
SDL_SetRenderViewport(renderer, nullptr);
|
SDL_SetRenderViewport(renderer, nullptr);
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
||||||
SDL_RenderClear(renderer);
|
SDL_RenderClear(renderer);
|
||||||
@ -1507,10 +1822,12 @@ void TetrisApp::Impl::runLoop()
|
|||||||
&pixelFont,
|
&pixelFont,
|
||||||
&lineEffect,
|
&lineEffect,
|
||||||
blocksTex,
|
blocksTex,
|
||||||
|
asteroidsTex,
|
||||||
ctx.statisticsPanelTex,
|
ctx.statisticsPanelTex,
|
||||||
scorePanelTex,
|
scorePanelTex,
|
||||||
nextPanelTex,
|
nextPanelTex,
|
||||||
holdPanelTex,
|
holdPanelTex,
|
||||||
|
false,
|
||||||
(float)LOGICAL_W,
|
(float)LOGICAL_W,
|
||||||
(float)LOGICAL_H,
|
(float)LOGICAL_H,
|
||||||
logicalScale,
|
logicalScale,
|
||||||
@ -1661,8 +1978,101 @@ void TetrisApp::Impl::runLoop()
|
|||||||
|
|
||||||
float textX = (winW - static_cast<float>(textW)) * 0.5f;
|
float textX = (winW - static_cast<float>(textW)) * 0.5f;
|
||||||
float textY = (winH - static_cast<float>(textH)) * 0.5f;
|
float textY = (winH - static_cast<float>(textH)) * 0.5f;
|
||||||
|
if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
|
||||||
|
char levelBuf[32];
|
||||||
|
std::snprintf(levelBuf, sizeof(levelBuf), "LEVEL %d", countdownLevel);
|
||||||
|
int lvlW = 0, lvlH = 0;
|
||||||
|
float lvlScale = 2.5f;
|
||||||
|
pixelFont.measure(levelBuf, lvlScale, lvlW, lvlH);
|
||||||
|
float levelX = (winW - static_cast<float>(lvlW)) * 0.5f;
|
||||||
|
float levelY = winH * 0.32f;
|
||||||
|
pixelFont.draw(renderer, levelX, levelY, levelBuf, lvlScale, SDL_Color{140, 210, 255, 255});
|
||||||
|
|
||||||
|
char goalBuf[64];
|
||||||
|
std::snprintf(goalBuf, sizeof(goalBuf), "ASTEROIDS: %d", countdownGoalAsteroids);
|
||||||
|
int goalW = 0, goalH = 0;
|
||||||
|
float goalScale = 1.7f;
|
||||||
|
pixelFont.measure(goalBuf, goalScale, goalW, goalH);
|
||||||
|
float goalX = (winW - static_cast<float>(goalW)) * 0.5f;
|
||||||
|
float goalY = levelY + static_cast<float>(lvlH) + 14.0f;
|
||||||
|
pixelFont.draw(renderer, goalX, goalY, goalBuf, goalScale, SDL_Color{220, 245, 255, 255});
|
||||||
|
|
||||||
|
// Optional story/briefing line
|
||||||
|
if (!challengeStoryText.empty() && challengeStoryAlpha > 0.0f) {
|
||||||
|
SDL_Color storyColor{170, 230, 255, static_cast<Uint8>(std::lround(255.0f * challengeStoryAlpha))};
|
||||||
|
SDL_Color shadowColor{0, 0, 0, static_cast<Uint8>(std::lround(160.0f * challengeStoryAlpha))};
|
||||||
|
|
||||||
|
auto drawCenteredWrapped = [&](const std::string& text, float y, float maxWidth, float scale) {
|
||||||
|
std::istringstream iss(text);
|
||||||
|
std::string word;
|
||||||
|
std::string line;
|
||||||
|
float cursorY = y;
|
||||||
|
int lastH = 0;
|
||||||
|
while (iss >> word) {
|
||||||
|
std::string candidate = line.empty() ? word : (line + " " + word);
|
||||||
|
int candidateW = 0, candidateH = 0;
|
||||||
|
pixelFont.measure(candidate, scale, candidateW, candidateH);
|
||||||
|
if (candidateW > maxWidth && !line.empty()) {
|
||||||
|
int lineW = 0, lineH = 0;
|
||||||
|
pixelFont.measure(line, scale, lineW, lineH);
|
||||||
|
float lineX = (winW - static_cast<float>(lineW)) * 0.5f;
|
||||||
|
pixelFont.draw(renderer, lineX + 1.0f, cursorY + 1.0f, line, scale, shadowColor);
|
||||||
|
pixelFont.draw(renderer, lineX, cursorY, line, scale, storyColor);
|
||||||
|
cursorY += lineH + 6.0f;
|
||||||
|
line = word;
|
||||||
|
lastH = lineH;
|
||||||
|
} else {
|
||||||
|
line = candidate;
|
||||||
|
lastH = candidateH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!line.empty()) {
|
||||||
|
int w = 0, h = 0;
|
||||||
|
pixelFont.measure(line, scale, w, h);
|
||||||
|
float lineX = (winW - static_cast<float>(w)) * 0.5f;
|
||||||
|
pixelFont.draw(renderer, lineX + 1.0f, cursorY + 1.0f, line, scale, shadowColor);
|
||||||
|
pixelFont.draw(renderer, lineX, cursorY, line, scale, storyColor);
|
||||||
|
cursorY += h + 6.0f;
|
||||||
|
}
|
||||||
|
return cursorY;
|
||||||
|
};
|
||||||
|
|
||||||
|
float storyStartY = goalY + static_cast<float>(goalH) + 22.0f;
|
||||||
|
float usedY = drawCenteredWrapped(challengeStoryText, storyStartY, std::min<float>(winW * 0.7f, 720.0f), 1.0f);
|
||||||
|
float promptY = usedY + 10.0f;
|
||||||
|
if (challengeCountdownWaitingForSpace) {
|
||||||
|
const char* prompt = "PRESS SPACE";
|
||||||
|
int pW = 0, pH = 0;
|
||||||
|
float pScale = 1.35f;
|
||||||
|
pixelFont.measure(prompt, pScale, pW, pH);
|
||||||
|
float px = (winW - static_cast<float>(pW)) * 0.5f;
|
||||||
|
pixelFont.draw(renderer, px + 2.0f, promptY + 2.0f, prompt, pScale, SDL_Color{0, 0, 0, 200});
|
||||||
|
pixelFont.draw(renderer, px, promptY, prompt, pScale, SDL_Color{255, 220, 40, 255});
|
||||||
|
promptY += pH + 14.0f;
|
||||||
|
}
|
||||||
|
textY = promptY + 10.0f;
|
||||||
|
} else {
|
||||||
|
if (challengeCountdownWaitingForSpace) {
|
||||||
|
const char* prompt = "PRESS SPACE";
|
||||||
|
int pW = 0, pH = 0;
|
||||||
|
float pScale = 1.35f;
|
||||||
|
pixelFont.measure(prompt, pScale, pW, pH);
|
||||||
|
float px = (winW - static_cast<float>(pW)) * 0.5f;
|
||||||
|
float py = goalY + static_cast<float>(goalH) + 18.0f;
|
||||||
|
pixelFont.draw(renderer, px + 2.0f, py + 2.0f, prompt, pScale, SDL_Color{0, 0, 0, 200});
|
||||||
|
pixelFont.draw(renderer, px, py, prompt, pScale, SDL_Color{255, 220, 40, 255});
|
||||||
|
textY = py + pH + 24.0f;
|
||||||
|
} else {
|
||||||
|
textY = goalY + static_cast<float>(goalH) + 38.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
textY = winH * 0.38f;
|
||||||
|
}
|
||||||
|
if (!(gameplayCountdownSource == CountdownSource::ChallengeLevel && challengeCountdownWaitingForSpace)) {
|
||||||
SDL_Color textColor = isFinalCue ? SDL_Color{255, 230, 90, 255} : SDL_Color{255, 255, 255, 255};
|
SDL_Color textColor = isFinalCue ? SDL_Color{255, 230, 90, 255} : SDL_Color{255, 255, 255, 255};
|
||||||
pixelFont.draw(renderer, textX, textY, label, textScale, textColor);
|
pixelFont.draw(renderer, textX, textY, label, textScale, textColor);
|
||||||
|
}
|
||||||
|
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,6 @@ bool SoundEffect::load(const std::string& filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
//std::printf("[SoundEffect] Loaded: %s (%d channels, %d Hz, %zu samples)\n", filePath.c_str(), channels, sampleRate, pcmData.size());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,8 +55,6 @@ void SoundEffect::play(float volume) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//std::printf("[SoundEffect] Playing sound with %zu samples at volume %.2f\n", pcmData.size(), volume);
|
|
||||||
|
|
||||||
// Calculate final volume
|
// Calculate final volume
|
||||||
float finalVolume = defaultVolume * volume;
|
float finalVolume = defaultVolume * volume;
|
||||||
finalVolume = (std::max)(0.0f, (std::min)(1.0f, finalVolume));
|
finalVolume = (std::max)(0.0f, (std::min)(1.0f, finalVolume));
|
||||||
|
|||||||
@ -643,6 +643,7 @@ bool ApplicationManager::initializeGame() {
|
|||||||
} else { m_stateContext.logoSmallW = 0; m_stateContext.logoSmallH = 0; }
|
} else { m_stateContext.logoSmallW = 0; m_stateContext.logoSmallH = 0; }
|
||||||
m_stateContext.backgroundTex = m_assetManager->getTexture("background");
|
m_stateContext.backgroundTex = m_assetManager->getTexture("background");
|
||||||
m_stateContext.blocksTex = m_assetManager->getTexture("blocks");
|
m_stateContext.blocksTex = m_assetManager->getTexture("blocks");
|
||||||
|
m_stateContext.asteroidsTex = m_assetManager->getTexture("asteroids");
|
||||||
m_stateContext.musicEnabled = &m_musicEnabled;
|
m_stateContext.musicEnabled = &m_musicEnabled;
|
||||||
m_stateContext.musicStarted = &m_musicStarted;
|
m_stateContext.musicStarted = &m_musicStarted;
|
||||||
m_stateContext.musicLoaded = &m_musicLoaded;
|
m_stateContext.musicLoaded = &m_musicLoaded;
|
||||||
@ -1162,10 +1163,12 @@ void ApplicationManager::setupStateHandlers() {
|
|||||||
m_stateContext.pixelFont,
|
m_stateContext.pixelFont,
|
||||||
m_stateContext.lineEffect,
|
m_stateContext.lineEffect,
|
||||||
m_stateContext.blocksTex,
|
m_stateContext.blocksTex,
|
||||||
|
m_stateContext.asteroidsTex,
|
||||||
m_stateContext.statisticsPanelTex,
|
m_stateContext.statisticsPanelTex,
|
||||||
m_stateContext.scorePanelTex,
|
m_stateContext.scorePanelTex,
|
||||||
m_stateContext.nextPanelTex,
|
m_stateContext.nextPanelTex,
|
||||||
m_stateContext.holdPanelTex,
|
m_stateContext.holdPanelTex,
|
||||||
|
false,
|
||||||
LOGICAL_W,
|
LOGICAL_W,
|
||||||
LOGICAL_H,
|
LOGICAL_H,
|
||||||
logicalScale,
|
logicalScale,
|
||||||
|
|||||||
@ -51,7 +51,11 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Game::reset(int startLevel_) {
|
void Game::reset(int startLevel_) {
|
||||||
|
// Standard reset is primarily for endless; Challenge reuses the same pipeline and then
|
||||||
|
// immediately sets up its own level state.
|
||||||
std::fill(board.begin(), board.end(), 0);
|
std::fill(board.begin(), board.end(), 0);
|
||||||
|
clearAsteroidGrid();
|
||||||
|
recentAsteroidExplosions.clear();
|
||||||
std::fill(blockCounts.begin(), blockCounts.end(), 0);
|
std::fill(blockCounts.begin(), blockCounts.end(), 0);
|
||||||
bag.clear();
|
bag.clear();
|
||||||
_score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_;
|
_score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_;
|
||||||
@ -59,6 +63,10 @@ void Game::reset(int startLevel_) {
|
|||||||
_currentCombo = 0;
|
_currentCombo = 0;
|
||||||
_maxCombo = 0;
|
_maxCombo = 0;
|
||||||
_comboCount = 0;
|
_comboCount = 0;
|
||||||
|
challengeComplete = false;
|
||||||
|
challengeLevelActive = false;
|
||||||
|
challengeAdvanceQueued = false;
|
||||||
|
challengeQueuedLevel = 0;
|
||||||
// Initialize gravity using NES timing table (ms per cell by level)
|
// Initialize gravity using NES timing table (ms per cell by level)
|
||||||
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
|
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
|
||||||
fallAcc = 0; gameOver=false; paused=false;
|
fallAcc = 0; gameOver=false; paused=false;
|
||||||
@ -72,6 +80,229 @@ void Game::reset(int startLevel_) {
|
|||||||
refillBag();
|
refillBag();
|
||||||
pieceSequence = 0;
|
pieceSequence = 0;
|
||||||
spawn();
|
spawn();
|
||||||
|
|
||||||
|
if (mode == GameMode::Challenge) {
|
||||||
|
int lvl = startLevel_ <= 0 ? 1 : startLevel_;
|
||||||
|
startChallengeRun(lvl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::clearAsteroidGrid() {
|
||||||
|
for (auto &cell : asteroidGrid) {
|
||||||
|
cell.reset();
|
||||||
|
}
|
||||||
|
asteroidsRemainingCount = 0;
|
||||||
|
asteroidsTotalThisLevel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::startChallengeRun(int startingLevel) {
|
||||||
|
mode = GameMode::Challenge;
|
||||||
|
int lvl = std::clamp(startingLevel, 1, ASTEROID_MAX_LEVEL);
|
||||||
|
// Reset all stats and timers like a fresh run
|
||||||
|
_score = 0; _lines = 0; _level = lvl; startLevel = lvl;
|
||||||
|
_tetrisesMade = 0;
|
||||||
|
_currentCombo = 0;
|
||||||
|
_maxCombo = 0;
|
||||||
|
_comboCount = 0;
|
||||||
|
_startTime = SDL_GetPerformanceCounter();
|
||||||
|
_pausedTime = 0;
|
||||||
|
_lastPauseStart = 0;
|
||||||
|
// Reseed challenge RNG so levels are deterministic per run but distinct per session
|
||||||
|
if (challengeSeedBase == 0) {
|
||||||
|
challengeSeedBase = static_cast<uint32_t>(SDL_GetTicks());
|
||||||
|
}
|
||||||
|
challengeRng.seed(challengeSeedBase + static_cast<uint32_t>(lvl));
|
||||||
|
challengeAdvanceQueued = false;
|
||||||
|
challengeQueuedLevel = 0;
|
||||||
|
setupChallengeLevel(lvl, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::beginNextChallengeLevel() {
|
||||||
|
if (mode != GameMode::Challenge || challengeComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
challengeAdvanceQueued = false;
|
||||||
|
challengeQueuedLevel = 0;
|
||||||
|
int next = challengeLevelIndex + 1;
|
||||||
|
if (next > ASTEROID_MAX_LEVEL) {
|
||||||
|
challengeComplete = true;
|
||||||
|
challengeLevelActive = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setupChallengeLevel(next, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setupChallengeLevel(int level, bool preserveStats) {
|
||||||
|
challengeLevelIndex = std::clamp(level, 1, ASTEROID_MAX_LEVEL);
|
||||||
|
_level = challengeLevelIndex;
|
||||||
|
startLevel = challengeLevelIndex;
|
||||||
|
challengeComplete = false;
|
||||||
|
challengeLevelActive = true;
|
||||||
|
challengeAdvanceQueued = false;
|
||||||
|
challengeQueuedLevel = 0;
|
||||||
|
// Refresh deterministic RNG for this level
|
||||||
|
challengeRng.seed(challengeSeedBase + static_cast<uint32_t>(challengeLevelIndex));
|
||||||
|
|
||||||
|
// Optionally reset cumulative stats (new run) or keep them (between levels)
|
||||||
|
if (!preserveStats) {
|
||||||
|
std::fill(blockCounts.begin(), blockCounts.end(), 0);
|
||||||
|
_score = 0;
|
||||||
|
_lines = 0;
|
||||||
|
_tetrisesMade = 0;
|
||||||
|
_currentCombo = 0;
|
||||||
|
_comboCount = 0;
|
||||||
|
_maxCombo = 0;
|
||||||
|
_startTime = SDL_GetPerformanceCounter();
|
||||||
|
_pausedTime = 0;
|
||||||
|
_lastPauseStart = 0;
|
||||||
|
} else {
|
||||||
|
_currentCombo = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear playfield and piece state
|
||||||
|
std::fill(board.begin(), board.end(), 0);
|
||||||
|
clearAsteroidGrid();
|
||||||
|
completedLines.clear();
|
||||||
|
hardDropCells.clear();
|
||||||
|
hardDropFxId = 0;
|
||||||
|
recentAsteroidExplosions.clear();
|
||||||
|
fallAcc = 0.0;
|
||||||
|
gameOver = false;
|
||||||
|
paused = false;
|
||||||
|
softDropping = false;
|
||||||
|
hold = Piece{};
|
||||||
|
hold.type = PIECE_COUNT;
|
||||||
|
canHold = true;
|
||||||
|
bag.clear();
|
||||||
|
refillBag();
|
||||||
|
pieceSequence = 0;
|
||||||
|
spawn();
|
||||||
|
|
||||||
|
// Challenge gravity scales upward per level (faster = smaller ms per cell)
|
||||||
|
double baseMs = gravityMsForLevel(0, gravityGlobalMultiplier);
|
||||||
|
double speedFactor = 1.0 + static_cast<double>(challengeLevelIndex) * 0.02;
|
||||||
|
gravityMs = (speedFactor > 0.0) ? (baseMs / speedFactor) : baseMs;
|
||||||
|
|
||||||
|
// Place asteroids for this level
|
||||||
|
placeAsteroidsForLevel(challengeLevelIndex);
|
||||||
|
|
||||||
|
if (levelUpCallback) {
|
||||||
|
levelUpCallback(_level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AsteroidType Game::chooseAsteroidTypeForLevel(int level) {
|
||||||
|
// Simple weight distribution by level bands
|
||||||
|
int normalWeight = 100;
|
||||||
|
int armoredWeight = 0;
|
||||||
|
int fallingWeight = 0;
|
||||||
|
int coreWeight = 0;
|
||||||
|
|
||||||
|
if (level >= 10) {
|
||||||
|
armoredWeight = 20;
|
||||||
|
normalWeight = 80;
|
||||||
|
}
|
||||||
|
if (level >= 20) {
|
||||||
|
fallingWeight = 20;
|
||||||
|
normalWeight = 60;
|
||||||
|
}
|
||||||
|
if (level >= 40) {
|
||||||
|
fallingWeight = 30;
|
||||||
|
armoredWeight = 25;
|
||||||
|
normalWeight = 45;
|
||||||
|
}
|
||||||
|
if (level >= 60) {
|
||||||
|
coreWeight = 20;
|
||||||
|
fallingWeight = 30;
|
||||||
|
armoredWeight = 25;
|
||||||
|
normalWeight = 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = normalWeight + armoredWeight + fallingWeight + coreWeight;
|
||||||
|
if (total <= 0) return AsteroidType::Normal;
|
||||||
|
std::uniform_int_distribution<int> dist(0, total - 1);
|
||||||
|
int pick = dist(challengeRng);
|
||||||
|
if (pick < normalWeight) return AsteroidType::Normal;
|
||||||
|
pick -= normalWeight;
|
||||||
|
if (pick < armoredWeight) return AsteroidType::Armored;
|
||||||
|
pick -= armoredWeight;
|
||||||
|
if (pick < fallingWeight) return AsteroidType::Falling;
|
||||||
|
return AsteroidType::Core;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsteroidCell Game::makeAsteroidForType(AsteroidType t) const {
|
||||||
|
AsteroidCell cell{};
|
||||||
|
cell.type = t;
|
||||||
|
switch (t) {
|
||||||
|
case AsteroidType::Normal:
|
||||||
|
cell.hitsRemaining = 1;
|
||||||
|
cell.gravityEnabled = false;
|
||||||
|
break;
|
||||||
|
case AsteroidType::Armored:
|
||||||
|
cell.hitsRemaining = 2;
|
||||||
|
cell.gravityEnabled = false;
|
||||||
|
break;
|
||||||
|
case AsteroidType::Falling:
|
||||||
|
cell.hitsRemaining = 2;
|
||||||
|
cell.gravityEnabled = false;
|
||||||
|
break;
|
||||||
|
case AsteroidType::Core:
|
||||||
|
cell.hitsRemaining = 3;
|
||||||
|
cell.gravityEnabled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cell.visualState = 0;
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::placeAsteroidsForLevel(int level) {
|
||||||
|
int desired = std::clamp(level, 1, ASTEROID_MAX_LEVEL);
|
||||||
|
// Placement window grows upward with level but caps at half board
|
||||||
|
int height = std::clamp(2 + level / 3, 2, ROWS / 2);
|
||||||
|
int minRow = ROWS - 1 - height;
|
||||||
|
int maxRow = ROWS - 1;
|
||||||
|
minRow = std::max(0, minRow);
|
||||||
|
|
||||||
|
std::uniform_int_distribution<int> xDist(0, COLS - 1);
|
||||||
|
std::uniform_int_distribution<int> yDist(minRow, maxRow);
|
||||||
|
|
||||||
|
int attempts = 0;
|
||||||
|
const int maxAttempts = desired * 16;
|
||||||
|
while (asteroidsRemainingCount < desired && attempts < maxAttempts) {
|
||||||
|
int x = xDist(challengeRng);
|
||||||
|
int y = yDist(challengeRng);
|
||||||
|
int idx = y * COLS + x;
|
||||||
|
attempts++;
|
||||||
|
if (board[idx] != 0 || asteroidGrid[idx].has_value()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
AsteroidType type = chooseAsteroidTypeForLevel(level);
|
||||||
|
AsteroidCell cell = makeAsteroidForType(type);
|
||||||
|
board[idx] = asteroidBoardValue(type);
|
||||||
|
asteroidGrid[idx] = cell;
|
||||||
|
++asteroidsRemainingCount;
|
||||||
|
++asteroidsTotalThisLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asteroidsRemainingCount < desired) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[CHALLENGE] Placed %d/%d asteroids for level %d", asteroidsRemainingCount, desired, level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper implementations for asteroid board encoding
|
||||||
|
bool Game::isAsteroidValue(int boardValue) {
|
||||||
|
return boardValue >= ASTEROID_BASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsteroidType Game::asteroidTypeFromValue(int boardValue) {
|
||||||
|
int idx = boardValue - ASTEROID_BASE;
|
||||||
|
if (idx < 0) return AsteroidType::Normal;
|
||||||
|
if (idx > static_cast<int>(AsteroidType::Core)) idx = static_cast<int>(AsteroidType::Core);
|
||||||
|
return static_cast<AsteroidType>(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Game::asteroidBoardValue(AsteroidType t) {
|
||||||
|
return ASTEROID_BASE + static_cast<int>(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
double Game::elapsed() const {
|
double Game::elapsed() const {
|
||||||
@ -113,6 +344,16 @@ void Game::setPaused(bool p) {
|
|||||||
paused = p;
|
paused = p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Game::consumeQueuedChallengeLevel() {
|
||||||
|
if (!challengeAdvanceQueued) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int next = challengeQueuedLevel;
|
||||||
|
challengeAdvanceQueued = false;
|
||||||
|
challengeQueuedLevel = 0;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
void Game::setSoftDropping(bool on) {
|
void Game::setSoftDropping(bool on) {
|
||||||
if (softDropping == on) {
|
if (softDropping == on) {
|
||||||
return;
|
return;
|
||||||
@ -235,6 +476,7 @@ void Game::lockPiece() {
|
|||||||
_tetrisesMade += 1;
|
_tetrisesMade += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode != GameMode::Challenge) {
|
||||||
// JS level progression (NES-like) using starting level rules
|
// JS level progression (NES-like) using starting level rules
|
||||||
// Both startLevel and _level are 0-based now.
|
// Both startLevel and _level are 0-based now.
|
||||||
int targetLevel = startLevel;
|
int targetLevel = startLevel;
|
||||||
@ -253,6 +495,10 @@ void Game::lockPiece() {
|
|||||||
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
|
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
|
||||||
if (levelUpCallback) levelUpCallback(_level);
|
if (levelUpCallback) levelUpCallback(_level);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Challenge keeps level tied to the current challenge stage; gravity already set there
|
||||||
|
_level = challengeLevelIndex;
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger sound effect callback for line clears
|
// Trigger sound effect callback for line clears
|
||||||
if (soundCallback) {
|
if (soundCallback) {
|
||||||
@ -283,6 +529,28 @@ int Game::checkLines() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-play asteroid destroy SFX immediately when a clearing line contains asteroids (reduces latency)
|
||||||
|
if (!completedLines.empty() && mode == GameMode::Challenge) {
|
||||||
|
std::optional<AsteroidType> foundType;
|
||||||
|
for (int y : completedLines) {
|
||||||
|
for (int x = 0; x < COLS; ++x) {
|
||||||
|
int idx = y * COLS + x;
|
||||||
|
if (isAsteroidValue(board[idx])) {
|
||||||
|
foundType = asteroidTypeFromValue(board[idx]);
|
||||||
|
} else if (idx >= 0 && idx < static_cast<int>(asteroidGrid.size()) && asteroidGrid[idx].has_value()) {
|
||||||
|
foundType = asteroidGrid[idx]->type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundType.has_value()) {
|
||||||
|
pendingAsteroidDestroyType = foundType;
|
||||||
|
if (!asteroidDestroySoundPreplayed && asteroidDestroyedCallback) {
|
||||||
|
asteroidDestroySoundPreplayed = true;
|
||||||
|
asteroidDestroyedCallback(*foundType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return static_cast<int>(completedLines.size());
|
return static_cast<int>(completedLines.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,31 +563,152 @@ void Game::clearCompletedLines() {
|
|||||||
|
|
||||||
void Game::actualClearLines() {
|
void Game::actualClearLines() {
|
||||||
if (completedLines.empty()) return;
|
if (completedLines.empty()) return;
|
||||||
|
recentAsteroidExplosions.clear();
|
||||||
|
|
||||||
int write = ROWS - 1;
|
std::array<int, COLS*ROWS> newBoard{};
|
||||||
|
std::array<std::optional<AsteroidCell>, COLS*ROWS> newAst{};
|
||||||
|
for (auto &cell : newAst) cell.reset();
|
||||||
|
std::fill(newBoard.begin(), newBoard.end(), 0);
|
||||||
|
|
||||||
|
handleAsteroidsOnClearedRows(completedLines, newBoard, newAst);
|
||||||
|
|
||||||
|
board = newBoard;
|
||||||
|
asteroidGrid = newAst;
|
||||||
|
|
||||||
|
// Apply asteroid-specific gravity after the board collapses
|
||||||
|
applyAsteroidGravity();
|
||||||
|
|
||||||
|
// Reset preplay latch so future destroys can fire again
|
||||||
|
pendingAsteroidDestroyType.reset();
|
||||||
|
asteroidDestroySoundPreplayed = false;
|
||||||
|
|
||||||
|
if (mode == GameMode::Challenge) {
|
||||||
|
if (asteroidsRemainingCount <= 0) {
|
||||||
|
int nextLevel = challengeLevelIndex + 1;
|
||||||
|
if (nextLevel > ASTEROID_MAX_LEVEL) {
|
||||||
|
challengeComplete = true;
|
||||||
|
challengeLevelActive = false;
|
||||||
|
challengeAdvanceQueued = false;
|
||||||
|
challengeQueuedLevel = 0;
|
||||||
|
} else {
|
||||||
|
challengeAdvanceQueued = true;
|
||||||
|
challengeQueuedLevel = nextLevel;
|
||||||
|
challengeLevelActive = false;
|
||||||
|
setPaused(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::handleAsteroidsOnClearedRows(const std::vector<int>& clearedRows,
|
||||||
|
std::array<int, COLS*ROWS>& outBoard,
|
||||||
|
std::array<std::optional<AsteroidCell>, COLS*ROWS>& outAsteroids) {
|
||||||
|
std::vector<bool> clearedFlags(ROWS, false);
|
||||||
|
for (int r : clearedRows) {
|
||||||
|
if (r >= 0 && r < ROWS) {
|
||||||
|
clearedFlags[r] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track asteroid count updates during processing
|
||||||
|
int destroyedThisPass = 0;
|
||||||
|
std::optional<AsteroidType> lastDestroyedType;
|
||||||
|
|
||||||
|
// Precompute how many cleared rows are at or below each row to reposition survivors
|
||||||
|
std::array<int, ROWS> clearedBelow{};
|
||||||
|
int running = 0;
|
||||||
for (int y = ROWS - 1; y >= 0; --y) {
|
for (int y = ROWS - 1; y >= 0; --y) {
|
||||||
// Check if this row should be cleared
|
clearedBelow[y] = running;
|
||||||
bool shouldClear = std::find(completedLines.begin(), completedLines.end(), y) != completedLines.end();
|
if (clearedFlags[y]) {
|
||||||
|
++running;
|
||||||
if (!shouldClear) {
|
|
||||||
// Keep this row, move it down if necessary
|
|
||||||
if (write != y) {
|
|
||||||
for (int x = 0; x < COLS; ++x) {
|
|
||||||
board[write*COLS + x] = board[y*COLS + x];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
--write;
|
|
||||||
}
|
|
||||||
// If shouldClear is true, we skip this row (effectively removing it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the top rows that are now empty
|
for (int y = ROWS - 1; y >= 0; --y) {
|
||||||
for (int y = write; y >= 0; --y) {
|
|
||||||
for (int x = 0; x < COLS; ++x) {
|
for (int x = 0; x < COLS; ++x) {
|
||||||
board[y*COLS + x] = 0;
|
int srcIdx = y * COLS + x;
|
||||||
|
bool rowCleared = clearedFlags[y];
|
||||||
|
bool isAsteroid = asteroidGrid[srcIdx].has_value();
|
||||||
|
|
||||||
|
if (rowCleared) {
|
||||||
|
if (!isAsteroid) {
|
||||||
|
continue; // normal blocks in cleared rows vanish
|
||||||
|
}
|
||||||
|
|
||||||
|
AsteroidCell cell = *asteroidGrid[srcIdx];
|
||||||
|
if (cell.hitsRemaining > 0) {
|
||||||
|
--cell.hitsRemaining;
|
||||||
|
}
|
||||||
|
if (cell.hitsRemaining == 0) {
|
||||||
|
destroyedThisPass++;
|
||||||
|
lastDestroyedType = cell.type;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update visual/gravity state for surviving asteroids
|
||||||
|
cell.visualState = static_cast<uint8_t>(std::min<int>(3, cell.visualState + 1));
|
||||||
|
if (cell.type == AsteroidType::Falling || cell.type == AsteroidType::Core) {
|
||||||
|
cell.gravityEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int destY = y + clearedBelow[y]; // shift down by cleared rows below
|
||||||
|
if (destY >= ROWS) {
|
||||||
|
continue; // off the board after collapse
|
||||||
|
}
|
||||||
|
int destIdx = destY * COLS + x;
|
||||||
|
outBoard[destIdx] = asteroidBoardValue(cell.type);
|
||||||
|
outAsteroids[destIdx] = cell;
|
||||||
|
} else {
|
||||||
|
int destY = y + clearedBelow[y];
|
||||||
|
if (destY >= ROWS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int destIdx = destY * COLS + x;
|
||||||
|
outBoard[destIdx] = board[srcIdx];
|
||||||
|
if (isAsteroid) {
|
||||||
|
outAsteroids[destIdx] = asteroidGrid[srcIdx];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destroyedThisPass > 0) {
|
||||||
|
asteroidsRemainingCount = std::max(0, asteroidsRemainingCount - destroyedThisPass);
|
||||||
|
if (!asteroidDestroySoundPreplayed && asteroidDestroyedCallback && lastDestroyedType.has_value()) {
|
||||||
|
asteroidDestroyedCallback(*lastDestroyedType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::applyAsteroidGravity() {
|
||||||
|
if (asteroidsRemainingCount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool moved = false;
|
||||||
|
do {
|
||||||
|
moved = false;
|
||||||
|
for (int y = ROWS - 2; y >= 0; --y) {
|
||||||
|
for (int x = 0; x < COLS; ++x) {
|
||||||
|
int idx = y * COLS + x;
|
||||||
|
if (!asteroidGrid[idx].has_value()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!asteroidGrid[idx]->gravityEnabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int belowIdx = (y + 1) * COLS + x;
|
||||||
|
if (board[belowIdx] == 0) {
|
||||||
|
// Move asteroid down one cell
|
||||||
|
board[belowIdx] = board[idx];
|
||||||
|
asteroidGrid[belowIdx] = asteroidGrid[idx];
|
||||||
|
board[idx] = 0;
|
||||||
|
asteroidGrid[idx].reset();
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (moved);
|
||||||
|
}
|
||||||
|
|
||||||
bool Game::tryMoveDown() {
|
bool Game::tryMoveDown() {
|
||||||
Piece p = cur; p.y += 1; if (!collides(p)) { cur = p; return true; } return false;
|
Piece p = cur; p.y += 1; if (!collides(p)) { cur = p; return true; } return false;
|
||||||
|
|||||||
@ -7,12 +7,26 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include "../../core/GravityManager.h"
|
#include "../../core/GravityManager.h"
|
||||||
|
|
||||||
enum PieceType { I, O, T, S, Z, J, L, PIECE_COUNT };
|
enum PieceType { I, O, T, S, Z, J, L, PIECE_COUNT };
|
||||||
using Shape = std::array<uint16_t, 4>; // four rotation bitmasks
|
using Shape = std::array<uint16_t, 4>; // four rotation bitmasks
|
||||||
|
|
||||||
|
// Game runtime mode
|
||||||
|
enum class GameMode { Endless, Challenge };
|
||||||
|
|
||||||
|
// Special obstacle blocks used by Challenge mode
|
||||||
|
enum class AsteroidType : uint8_t { Normal = 0, Armored = 1, Falling = 2, Core = 3 };
|
||||||
|
|
||||||
|
struct AsteroidCell {
|
||||||
|
AsteroidType type{AsteroidType::Normal};
|
||||||
|
uint8_t hitsRemaining{1};
|
||||||
|
bool gravityEnabled{false};
|
||||||
|
uint8_t visualState{0};
|
||||||
|
};
|
||||||
|
|
||||||
class Game {
|
class Game {
|
||||||
public:
|
public:
|
||||||
static constexpr int COLS = 10;
|
static constexpr int COLS = 10;
|
||||||
@ -21,8 +35,10 @@ public:
|
|||||||
|
|
||||||
struct Piece { PieceType type{PIECE_COUNT}; int rot{0}; int x{3}; int y{-2}; };
|
struct Piece { PieceType type{PIECE_COUNT}; int rot{0}; int x{3}; int y{-2}; };
|
||||||
|
|
||||||
explicit Game(int startLevel = 0) { reset(startLevel); }
|
explicit Game(int startLevel = 0, GameMode mode = GameMode::Endless) : mode(mode) { reset(startLevel); }
|
||||||
void reset(int startLevel = 0);
|
void reset(int startLevel = 0);
|
||||||
|
void startChallengeRun(int startingLevel = 1); // resets stats and starts challenge level 1 (or provided)
|
||||||
|
void beginNextChallengeLevel(); // advances to the next challenge level preserving score/time
|
||||||
|
|
||||||
// Simulation -----------------------------------------------------------
|
// Simulation -----------------------------------------------------------
|
||||||
void tickGravity(double frameMs); // advance gravity accumulator & drop
|
void tickGravity(double frameMs); // advance gravity accumulator & drop
|
||||||
@ -42,13 +58,26 @@ public:
|
|||||||
bool isGameOver() const { return gameOver; }
|
bool isGameOver() const { return gameOver; }
|
||||||
bool isPaused() const { return paused; }
|
bool isPaused() const { return paused; }
|
||||||
void setPaused(bool p);
|
void setPaused(bool p);
|
||||||
|
GameMode getMode() const { return mode; }
|
||||||
|
void setMode(GameMode m) { mode = m; }
|
||||||
int score() const { return _score; }
|
int score() const { return _score; }
|
||||||
int lines() const { return _lines; }
|
int lines() const { return _lines; }
|
||||||
int level() const { return _level; }
|
int level() const { return _level; }
|
||||||
|
int challengeLevel() const { return challengeLevelIndex; }
|
||||||
|
int asteroidsRemaining() const { return asteroidsRemainingCount; }
|
||||||
|
int asteroidsTotal() const { return asteroidsTotalThisLevel; }
|
||||||
|
bool isChallengeComplete() const { return challengeComplete; }
|
||||||
|
bool isChallengeLevelActive() const { return challengeLevelActive; }
|
||||||
|
bool isChallengeAdvanceQueued() const { return challengeAdvanceQueued; }
|
||||||
|
int queuedChallengeLevel() const { return challengeQueuedLevel; }
|
||||||
|
int consumeQueuedChallengeLevel(); // returns next level if queued, else 0
|
||||||
int startLevelBase() const { return startLevel; }
|
int startLevelBase() const { return startLevel; }
|
||||||
double elapsed() const; // Now calculated from start time
|
double elapsed() const; // Now calculated from start time
|
||||||
void updateElapsedTime(); // Update elapsed time from system clock
|
void updateElapsedTime(); // Update elapsed time from system clock
|
||||||
bool isSoftDropping() const { return softDropping; }
|
bool isSoftDropping() const { return softDropping; }
|
||||||
|
const std::array<std::optional<AsteroidCell>, COLS*ROWS>& asteroidCells() const { return asteroidGrid; }
|
||||||
|
const std::vector<SDL_Point>& getRecentAsteroidExplosions() const { return recentAsteroidExplosions; }
|
||||||
|
void clearRecentAsteroidExplosions() { recentAsteroidExplosions.clear(); }
|
||||||
|
|
||||||
// Block statistics
|
// Block statistics
|
||||||
const std::array<int, PIECE_COUNT>& getBlockCounts() const { return blockCounts; }
|
const std::array<int, PIECE_COUNT>& getBlockCounts() const { return blockCounts; }
|
||||||
@ -61,8 +90,10 @@ public:
|
|||||||
// Sound effect callbacks
|
// Sound effect callbacks
|
||||||
using SoundCallback = std::function<void(int)>; // Callback for line clear sounds (number of lines)
|
using SoundCallback = std::function<void(int)>; // Callback for line clear sounds (number of lines)
|
||||||
using LevelUpCallback = std::function<void(int)>; // Callback for level up sounds
|
using LevelUpCallback = std::function<void(int)>; // Callback for level up sounds
|
||||||
|
using AsteroidDestroyedCallback = std::function<void(AsteroidType)>; // Callback when an asteroid is fully destroyed
|
||||||
void setSoundCallback(SoundCallback callback) { soundCallback = callback; }
|
void setSoundCallback(SoundCallback callback) { soundCallback = callback; }
|
||||||
void setLevelUpCallback(LevelUpCallback callback) { levelUpCallback = callback; }
|
void setLevelUpCallback(LevelUpCallback callback) { levelUpCallback = callback; }
|
||||||
|
void setAsteroidDestroyedCallback(AsteroidDestroyedCallback callback) { asteroidDestroyedCallback = callback; }
|
||||||
|
|
||||||
// Shape helper --------------------------------------------------------
|
// Shape helper --------------------------------------------------------
|
||||||
static bool cellFilled(const Piece& p, int cx, int cy);
|
static bool cellFilled(const Piece& p, int cx, int cy);
|
||||||
@ -87,6 +118,9 @@ public:
|
|||||||
int comboCount() const { return _comboCount; }
|
int comboCount() const { return _comboCount; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
static constexpr int ASTEROID_BASE = 100; // sentinel offset for board encoding
|
||||||
|
static constexpr int ASTEROID_MAX_LEVEL = 100;
|
||||||
|
|
||||||
std::array<int, COLS*ROWS> board{}; // 0 empty else color index
|
std::array<int, COLS*ROWS> board{}; // 0 empty else color index
|
||||||
Piece cur{}, hold{}, nextPiece{}; // current, held & next piece
|
Piece cur{}, hold{}, nextPiece{}; // current, held & next piece
|
||||||
bool canHold{true};
|
bool canHold{true};
|
||||||
@ -117,6 +151,7 @@ private:
|
|||||||
// Sound effect callbacks
|
// Sound effect callbacks
|
||||||
SoundCallback soundCallback;
|
SoundCallback soundCallback;
|
||||||
LevelUpCallback levelUpCallback;
|
LevelUpCallback levelUpCallback;
|
||||||
|
AsteroidDestroyedCallback asteroidDestroyedCallback;
|
||||||
// Gravity tuning -----------------------------------------------------
|
// Gravity tuning -----------------------------------------------------
|
||||||
// Global multiplier applied to all level timings (use to slow/speed whole-game gravity)
|
// Global multiplier applied to all level timings (use to slow/speed whole-game gravity)
|
||||||
double gravityGlobalMultiplier{1.0};
|
double gravityGlobalMultiplier{1.0};
|
||||||
@ -132,6 +167,34 @@ private:
|
|||||||
uint32_t hardDropFxId{0};
|
uint32_t hardDropFxId{0};
|
||||||
uint64_t pieceSequence{0};
|
uint64_t pieceSequence{0};
|
||||||
|
|
||||||
|
// Challenge mode state -------------------------------------------------
|
||||||
|
GameMode mode{GameMode::Endless};
|
||||||
|
int challengeLevelIndex{1};
|
||||||
|
int asteroidsRemainingCount{0};
|
||||||
|
int asteroidsTotalThisLevel{0};
|
||||||
|
bool challengeComplete{false};
|
||||||
|
std::array<std::optional<AsteroidCell>, COLS*ROWS> asteroidGrid{};
|
||||||
|
uint32_t challengeSeedBase{0};
|
||||||
|
std::mt19937 challengeRng{ std::random_device{}() };
|
||||||
|
bool challengeLevelActive{false};
|
||||||
|
bool challengeAdvanceQueued{false};
|
||||||
|
int challengeQueuedLevel{0};
|
||||||
|
// Asteroid SFX latency mitigation
|
||||||
|
std::optional<AsteroidType> pendingAsteroidDestroyType;
|
||||||
|
bool asteroidDestroySoundPreplayed{false};
|
||||||
|
|
||||||
|
// Recent asteroid explosion positions (grid coords) for renderer FX
|
||||||
|
std::vector<SDL_Point> recentAsteroidExplosions;
|
||||||
|
|
||||||
|
// Expose the internal challenge seed base for deterministic FX/RNG coordination
|
||||||
|
public:
|
||||||
|
uint32_t getChallengeSeedBase() const { return challengeSeedBase; }
|
||||||
|
|
||||||
|
// Helpers for board encoding of asteroids
|
||||||
|
static bool isAsteroidValue(int boardValue);
|
||||||
|
static AsteroidType asteroidTypeFromValue(int boardValue);
|
||||||
|
static int asteroidBoardValue(AsteroidType t);
|
||||||
|
|
||||||
// Internal helpers ----------------------------------------------------
|
// Internal helpers ----------------------------------------------------
|
||||||
void refillBag();
|
void refillBag();
|
||||||
void spawn();
|
void spawn();
|
||||||
@ -140,5 +203,14 @@ private:
|
|||||||
int checkLines(); // Find completed lines and store them
|
int checkLines(); // Find completed lines and store them
|
||||||
void actualClearLines(); // Actually remove lines from board
|
void actualClearLines(); // Actually remove lines from board
|
||||||
bool tryMoveDown(); // one-row fall; returns true if moved
|
bool tryMoveDown(); // one-row fall; returns true if moved
|
||||||
|
void clearAsteroidGrid();
|
||||||
|
void setupChallengeLevel(int level, bool preserveStats);
|
||||||
|
void placeAsteroidsForLevel(int level);
|
||||||
|
AsteroidType chooseAsteroidTypeForLevel(int level);
|
||||||
|
AsteroidCell makeAsteroidForType(AsteroidType t) const;
|
||||||
|
void handleAsteroidsOnClearedRows(const std::vector<int>& clearedRows,
|
||||||
|
std::array<int, COLS*ROWS>& outBoard,
|
||||||
|
std::array<std::optional<AsteroidCell>, COLS*ROWS>& outAsteroids);
|
||||||
|
void applyAsteroidGravity();
|
||||||
// Gravity tuning helpers (public API declared above)
|
// Gravity tuning helpers (public API declared above)
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@ -49,6 +51,31 @@ Starfield3D s_inGridStarfield;
|
|||||||
bool s_starfieldInitialized = false;
|
bool s_starfieldInitialized = false;
|
||||||
std::vector<Sparkle> s_sparkles;
|
std::vector<Sparkle> s_sparkles;
|
||||||
float s_sparkleSpawnAcc = 0.0f;
|
float s_sparkleSpawnAcc = 0.0f;
|
||||||
|
|
||||||
|
struct AsteroidBurst {
|
||||||
|
float x;
|
||||||
|
float y;
|
||||||
|
float lifeMs;
|
||||||
|
float maxLifeMs;
|
||||||
|
float baseRadius;
|
||||||
|
SDL_Color color;
|
||||||
|
float spin;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<AsteroidBurst> s_asteroidBursts;
|
||||||
|
|
||||||
|
struct AsteroidShard {
|
||||||
|
float x;
|
||||||
|
float y;
|
||||||
|
float vx;
|
||||||
|
float vy;
|
||||||
|
float lifeMs;
|
||||||
|
float maxLifeMs;
|
||||||
|
float size;
|
||||||
|
SDL_Color color;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<AsteroidShard> s_asteroidShards;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TransportEffectState {
|
struct TransportEffectState {
|
||||||
@ -282,6 +309,62 @@ void GameRenderer::drawRect(SDL_Renderer* renderer, float x, float y, float w, f
|
|||||||
SDL_RenderFillRect(renderer, &fr);
|
SDL_RenderFillRect(renderer, &fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void drawAsteroid(SDL_Renderer* renderer, SDL_Texture* asteroidTex, float x, float y, float size, const AsteroidCell& cell) {
|
||||||
|
auto outlineGravity = [&](float inset, SDL_Color color) {
|
||||||
|
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
|
||||||
|
SDL_FRect glow{ x + inset, y + inset, size - inset * 2.0f, size - inset * 2.0f };
|
||||||
|
SDL_RenderRect(renderer, &glow);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (asteroidTex) {
|
||||||
|
const float SPRITE_SIZE = 90.0f;
|
||||||
|
int col = 0;
|
||||||
|
switch (cell.type) {
|
||||||
|
case AsteroidType::Normal: col = 0; break;
|
||||||
|
case AsteroidType::Armored: col = 1; break;
|
||||||
|
case AsteroidType::Falling: col = 2; break;
|
||||||
|
case AsteroidType::Core: col = 3; break;
|
||||||
|
}
|
||||||
|
int row = std::clamp<int>(cell.visualState, 0, 2);
|
||||||
|
SDL_FRect src{ col * SPRITE_SIZE, row * SPRITE_SIZE, SPRITE_SIZE, SPRITE_SIZE };
|
||||||
|
SDL_FRect dst{ x, y, size, size };
|
||||||
|
SDL_RenderTexture(renderer, asteroidTex, &src, &dst);
|
||||||
|
|
||||||
|
if (cell.gravityEnabled) {
|
||||||
|
outlineGravity(2.0f, SDL_Color{255, 230, 120, 180});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: draw a colored quad (previous implementation)
|
||||||
|
SDL_Color base{};
|
||||||
|
switch (cell.type) {
|
||||||
|
case AsteroidType::Normal: base = SDL_Color{172, 138, 104, 255}; break;
|
||||||
|
case AsteroidType::Armored: base = SDL_Color{130, 150, 176, 255}; break;
|
||||||
|
case AsteroidType::Falling: base = SDL_Color{210, 120, 82, 255}; break;
|
||||||
|
case AsteroidType::Core: base = SDL_Color{198, 78, 200, 255}; break;
|
||||||
|
}
|
||||||
|
float hpScale = std::clamp(static_cast<float>(cell.hitsRemaining) / 3.0f, 0.25f, 1.0f);
|
||||||
|
SDL_Color fill{
|
||||||
|
static_cast<Uint8>(base.r * hpScale + 40 * (1.0f - hpScale)),
|
||||||
|
static_cast<Uint8>(base.g * hpScale + 40 * (1.0f - hpScale)),
|
||||||
|
static_cast<Uint8>(base.b * hpScale + 40 * (1.0f - hpScale)),
|
||||||
|
255
|
||||||
|
};
|
||||||
|
SDL_SetRenderDrawColor(renderer, fill.r, fill.g, fill.b, fill.a);
|
||||||
|
SDL_FRect body{x, y, size - 1.0f, size - 1.0f};
|
||||||
|
SDL_RenderFillRect(renderer, &body);
|
||||||
|
|
||||||
|
SDL_Color outline = base;
|
||||||
|
outline.a = 220;
|
||||||
|
SDL_FRect border{x + 1.0f, y + 1.0f, size - 2.0f, size - 2.0f};
|
||||||
|
SDL_SetRenderDrawColor(renderer, outline.r, outline.g, outline.b, outline.a);
|
||||||
|
SDL_RenderRect(renderer, &border);
|
||||||
|
if (cell.gravityEnabled) {
|
||||||
|
outlineGravity(2.0f, SDL_Color{255, 230, 120, 180});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) {
|
void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) {
|
||||||
if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) {
|
if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) {
|
||||||
// Fallback to colored rectangle if texture isn't available
|
// Fallback to colored rectangle if texture isn't available
|
||||||
@ -515,15 +598,23 @@ void GameRenderer::renderPlayingState(
|
|||||||
FontAtlas* pixelFont,
|
FontAtlas* pixelFont,
|
||||||
LineEffect* lineEffect,
|
LineEffect* lineEffect,
|
||||||
SDL_Texture* blocksTex,
|
SDL_Texture* blocksTex,
|
||||||
|
SDL_Texture* asteroidsTex,
|
||||||
SDL_Texture* statisticsPanelTex,
|
SDL_Texture* statisticsPanelTex,
|
||||||
SDL_Texture* scorePanelTex,
|
SDL_Texture* scorePanelTex,
|
||||||
SDL_Texture* nextPanelTex,
|
SDL_Texture* nextPanelTex,
|
||||||
SDL_Texture* holdPanelTex,
|
SDL_Texture* holdPanelTex,
|
||||||
|
bool countdownActive,
|
||||||
float logicalW,
|
float logicalW,
|
||||||
float logicalH,
|
float logicalH,
|
||||||
float logicalScale,
|
float logicalScale,
|
||||||
float winW,
|
float winW,
|
||||||
float winH
|
float winH,
|
||||||
|
bool challengeClearFxActive,
|
||||||
|
const std::vector<int>* challengeClearFxOrder,
|
||||||
|
double challengeClearFxElapsedMs,
|
||||||
|
double challengeClearFxDurationMs,
|
||||||
|
const std::string* challengeStoryText,
|
||||||
|
float challengeStoryAlpha
|
||||||
) {
|
) {
|
||||||
if (!game || !pixelFont) return;
|
if (!game || !pixelFont) return;
|
||||||
|
|
||||||
@ -865,8 +956,67 @@ void GameRenderer::renderPlayingState(
|
|||||||
rowDropOffsets[y] = (lineEffect ? lineEffect->getRowDropOffset(y) : 0.0f);
|
rowDropOffsets[y] = (lineEffect ? lineEffect->getRowDropOffset(y) : 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spawn glamour bursts for freshly destroyed asteroids
|
||||||
|
if (game) {
|
||||||
|
const auto& bursts = game->getRecentAsteroidExplosions();
|
||||||
|
if (!bursts.empty()) {
|
||||||
|
std::uniform_real_distribution<float> lifeDist(280.0f, 420.0f);
|
||||||
|
std::uniform_real_distribution<float> radiusDist(finalBlockSize * 0.35f, finalBlockSize * 0.7f);
|
||||||
|
std::uniform_real_distribution<float> spinDist(-4.0f, 4.0f);
|
||||||
|
std::uniform_real_distribution<float> shardLife(240.0f, 520.0f);
|
||||||
|
std::uniform_real_distribution<float> shardVX(-0.16f, 0.16f);
|
||||||
|
std::uniform_real_distribution<float> shardVY(-0.22f, -0.06f);
|
||||||
|
std::uniform_real_distribution<float> shardSize(finalBlockSize * 0.06f, finalBlockSize * 0.12f);
|
||||||
|
for (const auto& p : bursts) {
|
||||||
|
if (p.x < 0 || p.x >= Game::COLS || p.y < 0 || p.y >= Game::ROWS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float fx = gridX + (static_cast<float>(p.x) + 0.5f) * finalBlockSize;
|
||||||
|
float fy = gridY + (static_cast<float>(p.y) + 0.5f) * finalBlockSize + rowDropOffsets[p.y];
|
||||||
|
|
||||||
|
SDL_Color palette[3] = {
|
||||||
|
SDL_Color{255, 230, 120, 255},
|
||||||
|
SDL_Color{140, 220, 255, 255},
|
||||||
|
SDL_Color{255, 160, 235, 255}
|
||||||
|
};
|
||||||
|
SDL_Color c = palette[s_impactRng() % 3];
|
||||||
|
AsteroidBurst burst{
|
||||||
|
fx,
|
||||||
|
fy,
|
||||||
|
lifeDist(s_impactRng),
|
||||||
|
0.0f,
|
||||||
|
radiusDist(s_impactRng),
|
||||||
|
c,
|
||||||
|
spinDist(s_impactRng)
|
||||||
|
};
|
||||||
|
burst.maxLifeMs = burst.lifeMs;
|
||||||
|
s_asteroidBursts.push_back(burst);
|
||||||
|
|
||||||
|
// Spawn shards for extra sparkle
|
||||||
|
int shardCount = 10 + (s_impactRng() % 8);
|
||||||
|
for (int i = 0; i < shardCount; ++i) {
|
||||||
|
AsteroidShard shard{
|
||||||
|
fx,
|
||||||
|
fy,
|
||||||
|
shardVX(s_impactRng),
|
||||||
|
shardVY(s_impactRng),
|
||||||
|
shardLife(s_impactRng),
|
||||||
|
0.0f,
|
||||||
|
shardSize(s_impactRng),
|
||||||
|
c
|
||||||
|
};
|
||||||
|
shard.maxLifeMs = shard.lifeMs;
|
||||||
|
s_asteroidShards.push_back(shard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
game->clearRecentAsteroidExplosions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw the game board
|
// Draw the game board
|
||||||
const auto &board = game->boardRef();
|
const auto &board = game->boardRef();
|
||||||
|
const auto &asteroidCells = game->asteroidCells();
|
||||||
|
const bool challengeMode = game->getMode() == GameMode::Challenge;
|
||||||
float impactStrength = 0.0f;
|
float impactStrength = 0.0f;
|
||||||
float impactEased = 0.0f;
|
float impactEased = 0.0f;
|
||||||
std::array<uint8_t, Game::COLS * Game::ROWS> impactMask{};
|
std::array<uint8_t, Game::COLS * Game::ROWS> impactMask{};
|
||||||
@ -937,6 +1087,25 @@ void GameRenderer::renderPlayingState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::array<float, Game::COLS * Game::ROWS> challengeClearMask{};
|
||||||
|
const bool challengeClearActive = challengeClearFxActive && challengeClearFxOrder && !challengeClearFxOrder->empty() && challengeClearFxDurationMs > 0.0;
|
||||||
|
if (challengeClearActive) {
|
||||||
|
const double totalDuration = std::max(50.0, challengeClearFxDurationMs);
|
||||||
|
const double perCell = totalDuration / static_cast<double>(challengeClearFxOrder->size());
|
||||||
|
for (size_t i = 0; i < challengeClearFxOrder->size(); ++i) {
|
||||||
|
int idx = (*challengeClearFxOrder)[i];
|
||||||
|
if (idx < 0 || idx >= static_cast<int>(challengeClearMask.size())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double startMs = perCell * static_cast<double>(i);
|
||||||
|
double local = (challengeClearFxElapsedMs - startMs) / perCell;
|
||||||
|
float progress = static_cast<float>(std::clamp(local, 0.0, 1.0));
|
||||||
|
if (progress > 0.0f) {
|
||||||
|
challengeClearMask[idx] = progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int y = 0; y < Game::ROWS; ++y) {
|
for (int y = 0; y < Game::ROWS; ++y) {
|
||||||
float dropOffset = rowDropOffsets[y];
|
float dropOffset = rowDropOffsets[y];
|
||||||
for (int x = 0; x < Game::COLS; ++x) {
|
for (int x = 0; x < Game::COLS; ++x) {
|
||||||
@ -954,10 +1123,133 @@ void GameRenderer::renderPlayingState(
|
|||||||
bx += amplitude * std::sin(t * freq);
|
bx += amplitude * std::sin(t * freq);
|
||||||
by += amplitude * 0.75f * std::cos(t * (freq + 1.1f));
|
by += amplitude * 0.75f * std::cos(t * (freq + 1.1f));
|
||||||
}
|
}
|
||||||
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1);
|
|
||||||
|
float clearProgress = challengeClearMask[cellIdx];
|
||||||
|
float clearAlpha = 1.0f;
|
||||||
|
float clearScale = 1.0f;
|
||||||
|
if (clearProgress > 0.0f) {
|
||||||
|
float eased = smoothstep(clearProgress);
|
||||||
|
clearAlpha = std::max(0.0f, 1.0f - eased);
|
||||||
|
clearScale = 1.0f + 0.35f * eased;
|
||||||
|
float offset = (finalBlockSize - finalBlockSize * clearScale) * 0.5f;
|
||||||
|
bx += offset;
|
||||||
|
by += offset;
|
||||||
|
float jitter = eased * 2.0f;
|
||||||
|
bx += std::sin(static_cast<float>(cellIdx) * 3.1f) * jitter;
|
||||||
|
by += std::cos(static_cast<float>(cellIdx) * 2.3f) * jitter * 0.6f;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAsteroid = challengeMode && asteroidCells[cellIdx].has_value();
|
||||||
|
if (isAsteroid) {
|
||||||
|
const AsteroidCell& cell = *asteroidCells[cellIdx];
|
||||||
|
float spawnScale = 1.0f;
|
||||||
|
float spawnAlpha = 1.0f;
|
||||||
|
if (countdownActive) {
|
||||||
|
// Staggered pop-in while counting: start oversized, fade to 1.0 with ease
|
||||||
|
const float t = static_cast<float>(SDL_GetTicks() & 2047) * 0.0015f; // ~0..3s loop
|
||||||
|
float phase = std::fmod(t + (float(cellIdx % 11) * 0.12f), 1.6f);
|
||||||
|
float pulse = std::clamp(phase / 1.2f, 0.0f, 1.0f);
|
||||||
|
float eased = smoothstep(pulse);
|
||||||
|
spawnScale = 1.35f - 0.35f * eased; // big -> normal
|
||||||
|
spawnAlpha = 0.25f + 0.75f * eased; // fade in
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asteroidsTex && spawnAlpha < 1.0f) {
|
||||||
|
SDL_SetTextureAlphaMod(asteroidsTex, static_cast<Uint8>(std::clamp(spawnAlpha, 0.0f, 1.0f) * 255.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
float size = finalBlockSize * spawnScale * clearScale;
|
||||||
|
float offset = (finalBlockSize - size) * 0.5f;
|
||||||
|
if (asteroidsTex && clearAlpha < 1.0f) {
|
||||||
|
Uint8 alpha = static_cast<Uint8>(std::clamp(spawnAlpha * clearAlpha, 0.0f, 1.0f) * 255.0f);
|
||||||
|
SDL_SetTextureAlphaMod(asteroidsTex, alpha);
|
||||||
|
}
|
||||||
|
drawAsteroid(renderer, asteroidsTex, bx + offset, by + offset, size, cell);
|
||||||
|
|
||||||
|
if (asteroidsTex && (spawnAlpha < 1.0f || clearAlpha < 1.0f)) {
|
||||||
|
SDL_SetTextureAlphaMod(asteroidsTex, 255);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (blocksTex && clearAlpha < 1.0f) {
|
||||||
|
SDL_SetTextureAlphaMod(blocksTex, static_cast<Uint8>(std::clamp(clearAlpha, 0.0f, 1.0f) * 255.0f));
|
||||||
|
}
|
||||||
|
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize * clearScale, v - 1);
|
||||||
|
if (blocksTex && clearAlpha < 1.0f) {
|
||||||
|
SDL_SetTextureAlphaMod(blocksTex, 255);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update & draw asteroid glamour shards and bursts
|
||||||
|
if (!s_asteroidShards.empty() || !s_asteroidBursts.empty()) {
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD);
|
||||||
|
|
||||||
|
// Shards
|
||||||
|
auto shardIt = s_asteroidShards.begin();
|
||||||
|
while (shardIt != s_asteroidShards.end()) {
|
||||||
|
AsteroidShard& s = *shardIt;
|
||||||
|
s.lifeMs -= sparkDeltaMs;
|
||||||
|
if (s.lifeMs <= 0.0f) {
|
||||||
|
shardIt = s_asteroidShards.erase(shardIt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
s.vy += 0.0007f * sparkDeltaMs;
|
||||||
|
s.x += s.vx * sparkDeltaMs;
|
||||||
|
s.y += s.vy * sparkDeltaMs;
|
||||||
|
float lifeRatio = std::clamp(static_cast<float>(s.lifeMs / s.maxLifeMs), 0.0f, 1.0f);
|
||||||
|
Uint8 alpha = static_cast<Uint8>(lifeRatio * 200.0f);
|
||||||
|
SDL_SetRenderDrawColor(renderer, s.color.r, s.color.g, s.color.b, alpha);
|
||||||
|
float size = s.size * (0.7f + (1.0f - lifeRatio) * 0.8f);
|
||||||
|
SDL_FRect shardRect{
|
||||||
|
s.x - size * 0.5f,
|
||||||
|
s.y - size * 0.5f,
|
||||||
|
size,
|
||||||
|
size * 1.4f
|
||||||
|
};
|
||||||
|
SDL_RenderFillRect(renderer, &shardRect);
|
||||||
|
++shardIt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bursts
|
||||||
|
auto it = s_asteroidBursts.begin();
|
||||||
|
while (it != s_asteroidBursts.end()) {
|
||||||
|
AsteroidBurst& b = *it;
|
||||||
|
b.lifeMs -= sparkDeltaMs;
|
||||||
|
if (b.lifeMs <= 0.0f) {
|
||||||
|
it = s_asteroidBursts.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float t = 1.0f - static_cast<float>(b.lifeMs / b.maxLifeMs);
|
||||||
|
float alpha = std::clamp(1.0f - t, 0.0f, 1.0f);
|
||||||
|
float radius = b.baseRadius * (1.0f + t * 1.6f);
|
||||||
|
float thickness = std::max(2.0f, radius * 0.25f);
|
||||||
|
float jitter = std::sin(t * 12.0f + b.spin) * 2.0f;
|
||||||
|
|
||||||
|
SDL_Color c = b.color;
|
||||||
|
Uint8 a = static_cast<Uint8>(alpha * 220.0f);
|
||||||
|
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, a);
|
||||||
|
SDL_FRect outer{
|
||||||
|
b.x - radius + jitter,
|
||||||
|
b.y - radius + jitter,
|
||||||
|
radius * 2.0f,
|
||||||
|
radius * 2.0f
|
||||||
|
};
|
||||||
|
SDL_RenderRect(renderer, &outer);
|
||||||
|
|
||||||
|
SDL_FRect inner{
|
||||||
|
b.x - (radius - thickness),
|
||||||
|
b.y - (radius - thickness),
|
||||||
|
(radius - thickness) * 2.0f,
|
||||||
|
(radius - thickness) * 2.0f
|
||||||
|
};
|
||||||
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, static_cast<Uint8>(a * 0.9f));
|
||||||
|
SDL_RenderRect(renderer, &inner);
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
|
}
|
||||||
|
|
||||||
if (!s_impactSparks.empty()) {
|
if (!s_impactSparks.empty()) {
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
@ -986,7 +1278,7 @@ void GameRenderer::renderPlayingState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool allowActivePieceRender = !GameRenderer::isTransportActive();
|
bool allowActivePieceRender = !GameRenderer::isTransportActive() && !challengeClearActive;
|
||||||
const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled();
|
const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled();
|
||||||
|
|
||||||
float activePiecePixelOffsetX = 0.0f;
|
float activePiecePixelOffsetX = 0.0f;
|
||||||
@ -1287,8 +1579,12 @@ void GameRenderer::renderPlayingState(
|
|||||||
|
|
||||||
char levelStr[16];
|
char levelStr[16];
|
||||||
snprintf(levelStr, sizeof(levelStr), "%02d", game->level());
|
snprintf(levelStr, sizeof(levelStr), "%02d", game->level());
|
||||||
|
char challengeLevelStr[16];
|
||||||
|
snprintf(challengeLevelStr, sizeof(challengeLevelStr), "%02d/100", game->challengeLevel());
|
||||||
|
char asteroidStr[32];
|
||||||
|
snprintf(asteroidStr, sizeof(asteroidStr), "%d LEFT", game->asteroidsRemaining());
|
||||||
|
|
||||||
// Next level progress
|
// Next level progress (endless only)
|
||||||
int startLv = game->startLevelBase();
|
int startLv = game->startLevelBase();
|
||||||
int firstThreshold = (startLv + 1) * 10;
|
int firstThreshold = (startLv + 1) * 10;
|
||||||
int linesDone = game->lines();
|
int linesDone = game->lines();
|
||||||
@ -1343,12 +1639,22 @@ void GameRenderer::renderPlayingState(
|
|||||||
statLines.push_back({scoreStr, 25.0f, 0.9f, valueColor});
|
statLines.push_back({scoreStr, 25.0f, 0.9f, valueColor});
|
||||||
statLines.push_back({"LINES", 70.0f, 1.0f, labelColor});
|
statLines.push_back({"LINES", 70.0f, 1.0f, labelColor});
|
||||||
statLines.push_back({linesStr, 95.0f, 0.9f, valueColor});
|
statLines.push_back({linesStr, 95.0f, 0.9f, valueColor});
|
||||||
|
|
||||||
|
if (game->getMode() == GameMode::Challenge) {
|
||||||
|
statLines.push_back({"LEVEL", 140.0f, 1.0f, labelColor});
|
||||||
|
statLines.push_back({challengeLevelStr, 165.0f, 0.9f, valueColor});
|
||||||
|
statLines.push_back({"ASTEROIDS", 200.0f, 1.0f, labelColor});
|
||||||
|
statLines.push_back({asteroidStr, 225.0f, 0.9f, nextColor});
|
||||||
|
statLines.push_back({"TIME", 265.0f, 1.0f, labelColor});
|
||||||
|
statLines.push_back({timeStr, 290.0f, 0.9f, valueColor});
|
||||||
|
} else {
|
||||||
statLines.push_back({"LEVEL", 140.0f, 1.0f, labelColor});
|
statLines.push_back({"LEVEL", 140.0f, 1.0f, labelColor});
|
||||||
statLines.push_back({levelStr, 165.0f, 0.9f, valueColor});
|
statLines.push_back({levelStr, 165.0f, 0.9f, valueColor});
|
||||||
statLines.push_back({"NEXT LVL", 200.0f, 1.0f, labelColor});
|
statLines.push_back({"NEXT LVL", 200.0f, 1.0f, labelColor});
|
||||||
statLines.push_back({nextStr, 225.0f, 0.9f, nextColor});
|
statLines.push_back({nextStr, 225.0f, 0.9f, nextColor});
|
||||||
statLines.push_back({"TIME", 265.0f, 1.0f, labelColor});
|
statLines.push_back({"TIME", 265.0f, 1.0f, labelColor});
|
||||||
statLines.push_back({timeStr, 290.0f, 0.9f, valueColor});
|
statLines.push_back({timeStr, 290.0f, 0.9f, valueColor});
|
||||||
|
}
|
||||||
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
SDL_Color debugLabelColor{150, 150, 150, 255};
|
SDL_Color debugLabelColor{150, 150, 150, 255};
|
||||||
@ -1400,6 +1706,51 @@ void GameRenderer::renderPlayingState(
|
|||||||
pixelFont->draw(renderer, statsTextX, baseY + line.offsetY, line.text, line.scale, line.color);
|
pixelFont->draw(renderer, statsTextX, baseY + line.offsetY, line.text, line.scale, line.color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Challenge story / briefing line near level indicator
|
||||||
|
if (challengeStoryText && !challengeStoryText->empty() && challengeStoryAlpha > 0.0f && game->getMode() == GameMode::Challenge) {
|
||||||
|
float alpha = std::clamp(challengeStoryAlpha, 0.0f, 1.0f);
|
||||||
|
SDL_Color storyColor{160, 220, 255, static_cast<Uint8>(std::lround(210.0f * alpha))};
|
||||||
|
SDL_Color shadowColor{0, 0, 0, static_cast<Uint8>(std::lround(120.0f * alpha))};
|
||||||
|
|
||||||
|
auto drawWrapped = [&](const std::string& text, float x, float y, float maxW, float scale, SDL_Color color) {
|
||||||
|
std::istringstream iss(text);
|
||||||
|
std::string word;
|
||||||
|
std::string line;
|
||||||
|
float cursorY = y;
|
||||||
|
int lastH = 0;
|
||||||
|
while (iss >> word) {
|
||||||
|
std::string candidate = line.empty() ? word : (line + " " + word);
|
||||||
|
int w = 0, h = 0;
|
||||||
|
pixelFont->measure(candidate, scale, w, h);
|
||||||
|
if (w > maxW && !line.empty()) {
|
||||||
|
pixelFont->draw(renderer, x + 1.0f, cursorY + 1.0f, line, scale, shadowColor);
|
||||||
|
pixelFont->draw(renderer, x, cursorY, line, scale, color);
|
||||||
|
cursorY += h + 4.0f;
|
||||||
|
line = word;
|
||||||
|
lastH = h;
|
||||||
|
} else {
|
||||||
|
line = candidate;
|
||||||
|
lastH = h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!line.empty()) {
|
||||||
|
pixelFont->draw(renderer, x + 1.0f, cursorY + 1.0f, line, scale, shadowColor);
|
||||||
|
pixelFont->draw(renderer, x, cursorY, line, scale, color);
|
||||||
|
cursorY += lastH + 4.0f;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
float storyX = statsTextX;
|
||||||
|
float storyY = baseY + 112.0f;
|
||||||
|
float maxW = 230.0f;
|
||||||
|
if (scorePanelMetricsValid && scorePanelWidth > 40.0f) {
|
||||||
|
storyX = scorePanelLeftX + 14.0f;
|
||||||
|
maxW = std::max(160.0f, scorePanelWidth - 28.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawWrapped(*challengeStoryText, storyX, storyY, maxW, 0.7f, storyColor);
|
||||||
|
}
|
||||||
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
pixelFont->draw(renderer, logicalW - 260, 10, gravityHud, 0.9f, {200, 200, 220, 255});
|
pixelFont->draw(renderer, logicalW - 260, 10, gravityHud, 0.9f, {200, 200, 220, 255});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
#include "../../gameplay/core/Game.h"
|
#include "../../gameplay/core/Game.h"
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
@ -21,15 +23,23 @@ public:
|
|||||||
FontAtlas* pixelFont,
|
FontAtlas* pixelFont,
|
||||||
LineEffect* lineEffect,
|
LineEffect* lineEffect,
|
||||||
SDL_Texture* blocksTex,
|
SDL_Texture* blocksTex,
|
||||||
|
SDL_Texture* asteroidsTex,
|
||||||
SDL_Texture* statisticsPanelTex,
|
SDL_Texture* statisticsPanelTex,
|
||||||
SDL_Texture* scorePanelTex,
|
SDL_Texture* scorePanelTex,
|
||||||
SDL_Texture* nextPanelTex,
|
SDL_Texture* nextPanelTex,
|
||||||
SDL_Texture* holdPanelTex,
|
SDL_Texture* holdPanelTex,
|
||||||
|
bool countdownActive,
|
||||||
float logicalW,
|
float logicalW,
|
||||||
float logicalH,
|
float logicalH,
|
||||||
float logicalScale,
|
float logicalScale,
|
||||||
float winW,
|
float winW,
|
||||||
float winH
|
float winH,
|
||||||
|
bool challengeClearFxActive = false,
|
||||||
|
const std::vector<int>* challengeClearFxOrder = nullptr,
|
||||||
|
double challengeClearFxElapsedMs = 0.0,
|
||||||
|
double challengeClearFxDurationMs = 0.0,
|
||||||
|
const std::string* challengeStoryText = nullptr,
|
||||||
|
float challengeStoryAlpha = 0.0f
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render the pause overlay (full screen)
|
// Render the pause overlay (full screen)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ namespace Assets {
|
|||||||
inline constexpr const char* LOGO = "assets/images/spacetris.png";
|
inline constexpr const char* LOGO = "assets/images/spacetris.png";
|
||||||
inline constexpr const char* MAIN_SCREEN = "assets/images/main_screen.png";
|
inline constexpr const char* MAIN_SCREEN = "assets/images/main_screen.png";
|
||||||
inline constexpr const char* BLOCKS_SPRITE = "assets/images/blocks90px_003.png";
|
inline constexpr const char* BLOCKS_SPRITE = "assets/images/blocks90px_003.png";
|
||||||
|
inline constexpr const char* ASTEROID_SPRITE = "assets/images/asteroids_001.png";
|
||||||
inline constexpr const char* PANEL_SCORE = "assets/images/panel_score.png";
|
inline constexpr const char* PANEL_SCORE = "assets/images/panel_score.png";
|
||||||
inline constexpr const char* PANEL_STATS = "assets/images/statistics_panel.png";
|
inline constexpr const char* PANEL_STATS = "assets/images/statistics_panel.png";
|
||||||
inline constexpr const char* NEXT_PANEL = "assets/images/next_panel.png";
|
inline constexpr const char* NEXT_PANEL = "assets/images/next_panel.png";
|
||||||
|
|||||||
@ -442,7 +442,7 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
|||||||
case SDL_SCANCODE_LEFT:
|
case SDL_SCANCODE_LEFT:
|
||||||
case SDL_SCANCODE_UP:
|
case SDL_SCANCODE_UP:
|
||||||
{
|
{
|
||||||
const int total = 6;
|
const int total = 7;
|
||||||
selectedButton = (selectedButton + total - 1) % total;
|
selectedButton = (selectedButton + total - 1) % total;
|
||||||
// brief bright flash on navigation
|
// brief bright flash on navigation
|
||||||
buttonFlash = 1.0;
|
buttonFlash = 1.0;
|
||||||
@ -451,7 +451,7 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
|||||||
case SDL_SCANCODE_RIGHT:
|
case SDL_SCANCODE_RIGHT:
|
||||||
case SDL_SCANCODE_DOWN:
|
case SDL_SCANCODE_DOWN:
|
||||||
{
|
{
|
||||||
const int total = 6;
|
const int total = 7;
|
||||||
selectedButton = (selectedButton + 1) % total;
|
selectedButton = (selectedButton + 1) % total;
|
||||||
// brief bright flash on navigation
|
// brief bright flash on navigation
|
||||||
buttonFlash = 1.0;
|
buttonFlash = 1.0;
|
||||||
@ -465,9 +465,22 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
|||||||
}
|
}
|
||||||
switch (selectedButton) {
|
switch (selectedButton) {
|
||||||
case 0:
|
case 0:
|
||||||
|
// Endless play
|
||||||
|
if (ctx.game) ctx.game->setMode(GameMode::Endless);
|
||||||
triggerPlay();
|
triggerPlay();
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
|
// Start challenge run at level 1
|
||||||
|
if (ctx.game) {
|
||||||
|
ctx.game->setMode(GameMode::Challenge);
|
||||||
|
if (ctx.skipNextLevelUpJingle) {
|
||||||
|
*ctx.skipNextLevelUpJingle = true;
|
||||||
|
}
|
||||||
|
ctx.game->startChallengeRun(1);
|
||||||
|
}
|
||||||
|
triggerPlay();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
// Toggle inline level selector HUD (show/hide)
|
// Toggle inline level selector HUD (show/hide)
|
||||||
if (!levelPanelVisible && !levelPanelAnimating) {
|
if (!levelPanelVisible && !levelPanelAnimating) {
|
||||||
levelPanelAnimating = true;
|
levelPanelAnimating = true;
|
||||||
@ -479,7 +492,7 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
|||||||
levelDirection = -1; // hide
|
levelDirection = -1; // hide
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 3:
|
||||||
// Toggle the options panel with an animated slide-in/out.
|
// Toggle the options panel with an animated slide-in/out.
|
||||||
if (!optionsVisible && !optionsAnimating) {
|
if (!optionsVisible && !optionsAnimating) {
|
||||||
optionsAnimating = true;
|
optionsAnimating = true;
|
||||||
@ -489,7 +502,7 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
|||||||
optionsDirection = -1; // hide
|
optionsDirection = -1; // hide
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 4:
|
||||||
// Toggle the inline HELP HUD (show/hide)
|
// Toggle the inline HELP HUD (show/hide)
|
||||||
if (!helpPanelVisible && !helpPanelAnimating) {
|
if (!helpPanelVisible && !helpPanelAnimating) {
|
||||||
helpPanelAnimating = true;
|
helpPanelAnimating = true;
|
||||||
@ -500,7 +513,7 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
|||||||
helpDirection = -1; // hide
|
helpDirection = -1; // hide
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 5:
|
||||||
// Toggle the inline ABOUT HUD (show/hide)
|
// Toggle the inline ABOUT HUD (show/hide)
|
||||||
if (!aboutPanelVisible && !aboutPanelAnimating) {
|
if (!aboutPanelVisible && !aboutPanelAnimating) {
|
||||||
aboutPanelAnimating = true;
|
aboutPanelAnimating = true;
|
||||||
@ -510,7 +523,7 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
|||||||
aboutDirection = -1;
|
aboutDirection = -1;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 6:
|
||||||
// Show the inline exit HUD
|
// Show the inline exit HUD
|
||||||
if (!exitPanelVisible && !exitPanelAnimating) {
|
if (!exitPanelVisible && !exitPanelAnimating) {
|
||||||
exitPanelAnimating = true;
|
exitPanelAnimating = true;
|
||||||
|
|||||||
@ -18,12 +18,18 @@ PlayingState::PlayingState(StateContext& ctx) : State(ctx) {}
|
|||||||
|
|
||||||
void PlayingState::onEnter() {
|
void PlayingState::onEnter() {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Entering Playing state");
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Entering Playing state");
|
||||||
// Initialize the game with the selected starting level
|
// Initialize the game based on mode: endless uses chosen start level, challenge keeps its run state
|
||||||
if (ctx.game && ctx.startLevelSelection) {
|
if (ctx.game) {
|
||||||
|
if (ctx.game->getMode() == GameMode::Endless) {
|
||||||
|
if (ctx.startLevelSelection) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection);
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection);
|
||||||
ctx.game->reset(*ctx.startLevelSelection);
|
ctx.game->reset(*ctx.startLevelSelection);
|
||||||
}
|
}
|
||||||
if (ctx.game) {
|
} else {
|
||||||
|
// Challenge run is prepared before entering; ensure gameplay is unpaused
|
||||||
|
ctx.game->setPaused(false);
|
||||||
|
}
|
||||||
|
|
||||||
s_lastPieceSequence = ctx.game->getCurrentPieceSequence();
|
s_lastPieceSequence = ctx.game->getCurrentPieceSequence();
|
||||||
s_pendingTransport = false;
|
s_pendingTransport = false;
|
||||||
}
|
}
|
||||||
@ -116,6 +122,16 @@ void PlayingState::handleEvent(const SDL_Event& e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug: skip to next challenge level (B)
|
||||||
|
if (e.key.scancode == SDL_SCANCODE_B && ctx.game && ctx.game->getMode() == GameMode::Challenge) {
|
||||||
|
ctx.game->beginNextChallengeLevel();
|
||||||
|
// Cancel any countdown so play resumes immediately on the new level
|
||||||
|
if (ctx.gameplayCountdownActive) *ctx.gameplayCountdownActive = false;
|
||||||
|
if (ctx.menuPlayCountdownArmed) *ctx.menuPlayCountdownArmed = false;
|
||||||
|
ctx.game->setPaused(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Tetris controls (only when not paused)
|
// Tetris controls (only when not paused)
|
||||||
if (!ctx.game->isPaused()) {
|
if (!ctx.game->isPaused()) {
|
||||||
// Hold / swap current piece (H)
|
// Hold / swap current piece (H)
|
||||||
@ -205,11 +221,15 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
|||||||
bool exitPopup = ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup;
|
bool exitPopup = ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup;
|
||||||
bool countdown = (ctx.gameplayCountdownActive && *ctx.gameplayCountdownActive) ||
|
bool countdown = (ctx.gameplayCountdownActive && *ctx.gameplayCountdownActive) ||
|
||||||
(ctx.menuPlayCountdownArmed && *ctx.menuPlayCountdownArmed);
|
(ctx.menuPlayCountdownArmed && *ctx.menuPlayCountdownArmed);
|
||||||
|
bool challengeClearFx = ctx.challengeClearFxActive && *ctx.challengeClearFxActive;
|
||||||
|
const std::vector<int>* challengeClearOrder = ctx.challengeClearFxOrder;
|
||||||
|
double challengeClearElapsed = ctx.challengeClearFxElapsedMs ? *ctx.challengeClearFxElapsedMs : 0.0;
|
||||||
|
double challengeClearDuration = ctx.challengeClearFxDurationMs ? *ctx.challengeClearFxDurationMs : 0.0;
|
||||||
|
|
||||||
// Only blur if paused AND NOT in countdown (and not exit popup, though exit popup implies paused)
|
// Only blur if paused AND NOT in countdown (and not exit popup, though exit popup implies paused)
|
||||||
// Actually, exit popup should probably still blur/dim.
|
// Actually, exit popup should probably still blur/dim.
|
||||||
// But countdown should definitely NOT show the "PAUSED" overlay.
|
// But countdown should definitely NOT show the "PAUSED" overlay.
|
||||||
bool shouldBlur = paused && !countdown;
|
bool shouldBlur = paused && !countdown && !challengeClearFx;
|
||||||
|
|
||||||
if (shouldBlur && m_renderTarget) {
|
if (shouldBlur && m_renderTarget) {
|
||||||
// Render game to texture
|
// Render game to texture
|
||||||
@ -235,15 +255,23 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
|||||||
ctx.pixelFont,
|
ctx.pixelFont,
|
||||||
ctx.lineEffect,
|
ctx.lineEffect,
|
||||||
ctx.blocksTex,
|
ctx.blocksTex,
|
||||||
|
ctx.asteroidsTex,
|
||||||
ctx.statisticsPanelTex,
|
ctx.statisticsPanelTex,
|
||||||
ctx.scorePanelTex,
|
ctx.scorePanelTex,
|
||||||
ctx.nextPanelTex,
|
ctx.nextPanelTex,
|
||||||
ctx.holdPanelTex,
|
ctx.holdPanelTex,
|
||||||
|
countdown,
|
||||||
1200.0f, // LOGICAL_W
|
1200.0f, // LOGICAL_W
|
||||||
1000.0f, // LOGICAL_H
|
1000.0f, // LOGICAL_H
|
||||||
logicalScale,
|
logicalScale,
|
||||||
(float)winW,
|
(float)winW,
|
||||||
(float)winH
|
(float)winH,
|
||||||
|
challengeClearFx,
|
||||||
|
challengeClearOrder,
|
||||||
|
challengeClearElapsed,
|
||||||
|
challengeClearDuration,
|
||||||
|
countdown ? nullptr : ctx.challengeStoryText,
|
||||||
|
countdown ? 0.0f : (ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reset to screen
|
// Reset to screen
|
||||||
@ -323,15 +351,23 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
|||||||
ctx.pixelFont,
|
ctx.pixelFont,
|
||||||
ctx.lineEffect,
|
ctx.lineEffect,
|
||||||
ctx.blocksTex,
|
ctx.blocksTex,
|
||||||
|
ctx.asteroidsTex,
|
||||||
ctx.statisticsPanelTex,
|
ctx.statisticsPanelTex,
|
||||||
ctx.scorePanelTex,
|
ctx.scorePanelTex,
|
||||||
ctx.nextPanelTex,
|
ctx.nextPanelTex,
|
||||||
ctx.holdPanelTex,
|
ctx.holdPanelTex,
|
||||||
|
countdown,
|
||||||
1200.0f,
|
1200.0f,
|
||||||
1000.0f,
|
1000.0f,
|
||||||
logicalScale,
|
logicalScale,
|
||||||
(float)winW,
|
(float)winW,
|
||||||
(float)winH
|
(float)winH,
|
||||||
|
challengeClearFx,
|
||||||
|
challengeClearOrder,
|
||||||
|
challengeClearElapsed,
|
||||||
|
challengeClearDuration,
|
||||||
|
countdown ? nullptr : ctx.challengeStoryText,
|
||||||
|
countdown ? 0.0f : (ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ struct StateContext {
|
|||||||
// backgroundTex is set once in `main.cpp` and passed to states via this context.
|
// backgroundTex is set once in `main.cpp` and passed to states via this context.
|
||||||
// Prefer reading this field instead of relying on any `extern SDL_Texture*` globals.
|
// Prefer reading this field instead of relying on any `extern SDL_Texture*` globals.
|
||||||
SDL_Texture* blocksTex = nullptr;
|
SDL_Texture* blocksTex = nullptr;
|
||||||
|
SDL_Texture* asteroidsTex = nullptr;
|
||||||
SDL_Texture* scorePanelTex = nullptr;
|
SDL_Texture* scorePanelTex = nullptr;
|
||||||
SDL_Texture* statisticsPanelTex = nullptr;
|
SDL_Texture* statisticsPanelTex = nullptr;
|
||||||
SDL_Texture* nextPanelTex = nullptr;
|
SDL_Texture* nextPanelTex = nullptr;
|
||||||
@ -66,6 +67,15 @@ struct StateContext {
|
|||||||
int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default)
|
int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default)
|
||||||
bool* gameplayCountdownActive = nullptr; // True if start-of-game countdown is running
|
bool* gameplayCountdownActive = nullptr; // True if start-of-game countdown is running
|
||||||
bool* menuPlayCountdownArmed = nullptr; // True if we are transitioning to play and countdown is pending
|
bool* menuPlayCountdownArmed = nullptr; // True if we are transitioning to play and countdown is pending
|
||||||
|
bool* skipNextLevelUpJingle = nullptr; // Allows states to silence initial level-up SFX
|
||||||
|
// Challenge clear FX (slow block-by-block explosion before next level)
|
||||||
|
bool* challengeClearFxActive = nullptr;
|
||||||
|
double* challengeClearFxElapsedMs = nullptr;
|
||||||
|
double* challengeClearFxDurationMs = nullptr;
|
||||||
|
std::vector<int>* challengeClearFxOrder = nullptr;
|
||||||
|
std::string* challengeStoryText = nullptr; // Per-level briefing string for Challenge mode
|
||||||
|
int* challengeStoryLevel = nullptr; // Cached level for the current story line
|
||||||
|
float* challengeStoryAlpha = nullptr; // Current render alpha for story text fade
|
||||||
std::string* playerName = nullptr; // Shared player name buffer for highscores/options
|
std::string* playerName = nullptr; // Shared player name buffer for highscores/options
|
||||||
bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available
|
bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available
|
||||||
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes
|
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes
|
||||||
|
|||||||
@ -22,11 +22,12 @@ BottomMenu buildBottomMenu(const MenuLayoutParams& params, int startLevel) {
|
|||||||
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
||||||
|
|
||||||
menu.buttons[0] = Button{ BottomMenuItem::Play, rects[0], "PLAY", false };
|
menu.buttons[0] = Button{ BottomMenuItem::Play, rects[0], "PLAY", false };
|
||||||
menu.buttons[1] = Button{ BottomMenuItem::Level, rects[1], levelBtnText, true };
|
menu.buttons[1] = Button{ BottomMenuItem::Challenge, rects[1], "CHALLENGE", false };
|
||||||
menu.buttons[2] = Button{ BottomMenuItem::Options, rects[2], "OPTIONS", true };
|
menu.buttons[2] = Button{ BottomMenuItem::Level, rects[2], levelBtnText, true };
|
||||||
menu.buttons[3] = Button{ BottomMenuItem::Help, rects[3], "HELP", true };
|
menu.buttons[3] = Button{ BottomMenuItem::Options, rects[3], "OPTIONS", true };
|
||||||
menu.buttons[4] = Button{ BottomMenuItem::About, rects[4], "ABOUT", true };
|
menu.buttons[4] = Button{ BottomMenuItem::Help, rects[4], "HELP", true };
|
||||||
menu.buttons[5] = Button{ BottomMenuItem::Exit, rects[5], "EXIT", true };
|
menu.buttons[5] = Button{ BottomMenuItem::About, rects[5], "ABOUT", true };
|
||||||
|
menu.buttons[6] = Button{ BottomMenuItem::Exit, rects[6], "EXIT", true };
|
||||||
|
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
@ -60,8 +61,15 @@ void renderBottomMenu(SDL_Renderer* renderer,
|
|||||||
const double aMul = std::clamp(baseMul + (playIsActive ? flashMul : 0.0), 0.0, 1.0);
|
const double aMul = std::clamp(baseMul + (playIsActive ? flashMul : 0.0), 0.0, 1.0);
|
||||||
|
|
||||||
if (!b.textOnly) {
|
if (!b.textOnly) {
|
||||||
|
const bool isPlay = (i == 0);
|
||||||
|
const bool isChallenge = (i == 1);
|
||||||
SDL_Color bgCol{ 18, 22, 28, static_cast<Uint8>(std::round(180.0 * aMul)) };
|
SDL_Color bgCol{ 18, 22, 28, static_cast<Uint8>(std::round(180.0 * aMul)) };
|
||||||
SDL_Color bdCol{ 255, 200, 70, static_cast<Uint8>(std::round(220.0 * aMul)) };
|
SDL_Color bdCol{ 255, 200, 70, static_cast<Uint8>(std::round(220.0 * aMul)) };
|
||||||
|
if (isChallenge) {
|
||||||
|
// Give Challenge a teal accent to distinguish from Play
|
||||||
|
bgCol = SDL_Color{ 18, 36, 36, static_cast<Uint8>(std::round(190.0 * aMul)) };
|
||||||
|
bdCol = SDL_Color{ 120, 255, 220, static_cast<Uint8>(std::round(230.0 * aMul)) };
|
||||||
|
}
|
||||||
UIRenderer::drawButton(renderer, font, cx, cy, r.w, r.h,
|
UIRenderer::drawButton(renderer, font, cx, cy, r.w, r.h,
|
||||||
b.label, isHovered, isSelected,
|
b.label, isHovered, isSelected,
|
||||||
bgCol, bdCol, false, nullptr);
|
bgCol, bdCol, false, nullptr);
|
||||||
@ -74,14 +82,14 @@ void renderBottomMenu(SDL_Renderer* renderer,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// '+' separators between the bottom HUD buttons (indices 1..last)
|
// '+' separators between the bottom HUD buttons (indices 2..last)
|
||||||
{
|
{
|
||||||
SDL_BlendMode prevBlend = SDL_BLENDMODE_NONE;
|
SDL_BlendMode prevBlend = SDL_BLENDMODE_NONE;
|
||||||
SDL_GetRenderDrawBlendMode(renderer, &prevBlend);
|
SDL_GetRenderDrawBlendMode(renderer, &prevBlend);
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
SDL_SetRenderDrawColor(renderer, 120, 220, 255, static_cast<Uint8>(std::round(180.0 * baseMul)));
|
SDL_SetRenderDrawColor(renderer, 120, 220, 255, static_cast<Uint8>(std::round(180.0 * baseMul)));
|
||||||
|
|
||||||
const int firstSmall = 1;
|
const int firstSmall = 2;
|
||||||
const int lastSmall = MENU_BTN_COUNT - 1;
|
const int lastSmall = MENU_BTN_COUNT - 1;
|
||||||
float y = menu.buttons[firstSmall].rect.y + menu.buttons[firstSmall].rect.h * 0.5f;
|
float y = menu.buttons[firstSmall].rect.y + menu.buttons[firstSmall].rect.h * 0.5f;
|
||||||
for (int i = firstSmall; i < lastSmall; ++i) {
|
for (int i = firstSmall; i < lastSmall; ++i) {
|
||||||
|
|||||||
@ -15,11 +15,12 @@ namespace ui {
|
|||||||
|
|
||||||
enum class BottomMenuItem : int {
|
enum class BottomMenuItem : int {
|
||||||
Play = 0,
|
Play = 0,
|
||||||
Level = 1,
|
Challenge = 1,
|
||||||
Options = 2,
|
Level = 2,
|
||||||
Help = 3,
|
Options = 3,
|
||||||
About = 4,
|
Help = 4,
|
||||||
Exit = 5,
|
About = 5,
|
||||||
|
Exit = 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Button {
|
struct Button {
|
||||||
|
|||||||
@ -12,28 +12,32 @@ std::array<SDL_FRect, MENU_BTN_COUNT> computeMenuButtonRects(const MenuLayoutPar
|
|||||||
float contentOffsetY = (p.winH - LOGICAL_H * p.logicalScale) * 0.5f / p.logicalScale;
|
float contentOffsetY = (p.winH - LOGICAL_H * p.logicalScale) * 0.5f / p.logicalScale;
|
||||||
|
|
||||||
// Cockpit HUD layout (matches main_screen art):
|
// Cockpit HUD layout (matches main_screen art):
|
||||||
// - A big centered PLAY button
|
// - Top row: PLAY and CHALLENGE (big buttons)
|
||||||
// - A second row of 5 smaller buttons: LEVEL / OPTIONS / HELP / ABOUT / EXIT
|
// - Second row: LEVEL / OPTIONS / HELP / ABOUT / EXIT (smaller buttons)
|
||||||
const float marginX = std::max(24.0f, LOGICAL_W * 0.03f);
|
const float marginX = std::max(24.0f, LOGICAL_W * 0.03f);
|
||||||
const float marginBottom = std::max(26.0f, LOGICAL_H * 0.03f);
|
const float marginBottom = std::max(26.0f, LOGICAL_H * 0.03f);
|
||||||
const float availableW = std::max(120.0f, LOGICAL_W - marginX * 2.0f);
|
const float availableW = std::max(120.0f, LOGICAL_W - marginX * 2.0f);
|
||||||
|
|
||||||
float playW = std::min(230.0f, availableW * 0.27f);
|
float playW = std::min(220.0f, availableW * 0.25f);
|
||||||
float playH = 35.0f;
|
float playH = 36.0f;
|
||||||
float smallW = std::min(220.0f, availableW * 0.23f);
|
float bigGap = 28.0f;
|
||||||
|
float smallW = std::min(210.0f, availableW * 0.22f);
|
||||||
float smallH = 34.0f;
|
float smallH = 34.0f;
|
||||||
float smallSpacing = 28.0f;
|
float smallSpacing = 26.0f;
|
||||||
|
|
||||||
// Scale down for narrow windows so nothing goes offscreen.
|
// Scale down for narrow windows so nothing goes offscreen.
|
||||||
const int smallCount = MENU_BTN_COUNT - 1;
|
const int smallCount = MENU_BTN_COUNT - 2;
|
||||||
float smallTotal = smallW * static_cast<float>(smallCount) + smallSpacing * static_cast<float>(smallCount - 1);
|
float smallTotal = smallW * static_cast<float>(smallCount) + smallSpacing * static_cast<float>(smallCount - 1);
|
||||||
if (smallTotal > availableW) {
|
float topRowTotal = playW * 2.0f + bigGap;
|
||||||
float s = availableW / smallTotal;
|
if (smallTotal > availableW || topRowTotal > availableW) {
|
||||||
|
float s = availableW / std::max(std::max(smallTotal, topRowTotal), 1.0f);
|
||||||
smallW *= s;
|
smallW *= s;
|
||||||
smallH *= s;
|
smallH *= s;
|
||||||
smallSpacing *= s;
|
smallSpacing *= s;
|
||||||
|
playW *= s;
|
||||||
|
playH = std::max(26.0f, playH * std::max(0.75f, s));
|
||||||
|
bigGap *= s;
|
||||||
playW = std::min(playW, availableW);
|
playW = std::min(playW, availableW);
|
||||||
playH *= std::max(0.75f, s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float centerX = LOGICAL_W * 0.5f + contentOffsetX;
|
float centerX = LOGICAL_W * 0.5f + contentOffsetX;
|
||||||
@ -44,7 +48,11 @@ std::array<SDL_FRect, MENU_BTN_COUNT> computeMenuButtonRects(const MenuLayoutPar
|
|||||||
float playCY = smallCY - smallH * 0.5f - rowGap - playH * 0.5f;
|
float playCY = smallCY - smallH * 0.5f - rowGap - playH * 0.5f;
|
||||||
|
|
||||||
std::array<SDL_FRect, MENU_BTN_COUNT> rects{};
|
std::array<SDL_FRect, MENU_BTN_COUNT> rects{};
|
||||||
rects[0] = SDL_FRect{ centerX - playW * 0.5f, playCY - playH * 0.5f, playW, playH };
|
// Top row big buttons
|
||||||
|
float playLeft = centerX - (playW + bigGap * 0.5f);
|
||||||
|
float challengeLeft = centerX + bigGap * 0.5f;
|
||||||
|
rects[0] = SDL_FRect{ playLeft, playCY - playH * 0.5f, playW, playH };
|
||||||
|
rects[1] = SDL_FRect{ challengeLeft, playCY - playH * 0.5f, playW, playH };
|
||||||
|
|
||||||
float rowW = smallW * static_cast<float>(smallCount) + smallSpacing * static_cast<float>(smallCount - 1);
|
float rowW = smallW * static_cast<float>(smallCount) + smallSpacing * static_cast<float>(smallCount - 1);
|
||||||
float left = centerX - rowW * 0.5f;
|
float left = centerX - rowW * 0.5f;
|
||||||
@ -55,7 +63,7 @@ std::array<SDL_FRect, MENU_BTN_COUNT> computeMenuButtonRects(const MenuLayoutPar
|
|||||||
|
|
||||||
for (int i = 0; i < smallCount; ++i) {
|
for (int i = 0; i < smallCount; ++i) {
|
||||||
float x = left + i * (smallW + smallSpacing);
|
float x = left + i * (smallW + smallSpacing);
|
||||||
rects[i + 1] = SDL_FRect{ x, smallCY - smallH * 0.5f, smallW, smallH };
|
rects[i + 2] = SDL_FRect{ x, smallCY - smallH * 0.5f, smallW, smallH };
|
||||||
}
|
}
|
||||||
return rects;
|
return rects;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
static constexpr int MENU_BTN_COUNT = 6;
|
static constexpr int MENU_BTN_COUNT = 7;
|
||||||
static constexpr float MENU_SMALL_THRESHOLD = 700.0f;
|
static constexpr float MENU_SMALL_THRESHOLD = 700.0f;
|
||||||
static constexpr float MENU_BTN_WIDTH_LARGE = 300.0f;
|
static constexpr float MENU_BTN_WIDTH_LARGE = 300.0f;
|
||||||
static constexpr float MENU_BTN_WIDTH_SMALL_FACTOR = 0.4f; // multiplied by LOGICAL_W
|
static constexpr float MENU_BTN_WIDTH_SMALL_FACTOR = 0.4f; // multiplied by LOGICAL_W
|
||||||
|
|||||||
Reference in New Issue
Block a user