Merge branch 'feature/RedesignMainScene' into develop
@ -44,9 +44,11 @@ set(TETRIS_SOURCES
|
||||
src/persistence/Scores.cpp
|
||||
src/graphics/effects/Starfield.cpp
|
||||
src/graphics/effects/Starfield3D.cpp
|
||||
src/graphics/effects/SpaceWarp.cpp
|
||||
src/graphics/ui/Font.cpp
|
||||
src/graphics/ui/HelpOverlay.cpp
|
||||
src/graphics/renderers/GameRenderer.cpp
|
||||
src/graphics/renderers/UIRenderer.cpp
|
||||
src/audio/Audio.cpp
|
||||
src/gameplay/effects/LineEffect.cpp
|
||||
src/audio/SoundEffect.cpp
|
||||
|
||||
760
CODE_ANALYSIS.md
Normal file
@ -0,0 +1,760 @@
|
||||
# Tetris SDL3 - Code Analysis & Best Practices Review
|
||||
|
||||
**Generated:** 2025-12-03
|
||||
**Project:** Tetris Game (SDL3)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Executive Summary
|
||||
|
||||
Your Tetris project is **well-structured and follows many modern C++ best practices**. The codebase demonstrates:
|
||||
- ✅ Clean separation of concerns with a state-based architecture
|
||||
- ✅ Modern C++20 features and RAII patterns
|
||||
- ✅ Centralized configuration management
|
||||
- ✅ Proper dependency management via vcpkg
|
||||
- ✅ Good documentation and code organization
|
||||
|
||||
However, there are opportunities for improvement in areas like memory management, error handling, and code duplication.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Strengths
|
||||
|
||||
### 1. **Architecture & Design Patterns**
|
||||
- **State Pattern Implementation**: Clean state management with `MenuState`, `PlayingState`, `OptionsState`, `LevelSelectorState`, and `LoadingState`
|
||||
- **Separation of Concerns**: Game logic (`Game.cpp`), rendering (`GameRenderer`, `UIRenderer`), audio (`Audio`, `SoundEffect`), and persistence (`Scores`) are well-separated
|
||||
- **Centralized Configuration**: `Config.h` provides a single source of truth for constants, eliminating magic numbers
|
||||
- **Service Locator Pattern**: `StateContext` acts as a dependency injection container
|
||||
|
||||
### 2. **Modern C++ Practices**
|
||||
- **C++20 Standard**: Using modern features like `std::filesystem`, `std::jthread`
|
||||
- **RAII**: Proper resource management with smart pointers and automatic cleanup
|
||||
- **Type Safety**: Strong typing with enums (`PieceType`, `AppState`, `LevelBackgroundPhase`)
|
||||
- **Const Correctness**: Good use of `const` methods and references
|
||||
|
||||
### 3. **Code Organization**
|
||||
```
|
||||
src/
|
||||
├── audio/ # Audio system (music, sound effects)
|
||||
├── core/ # Core systems (state management, settings, global state)
|
||||
├── gameplay/ # Game logic (Tetris mechanics, effects)
|
||||
├── graphics/ # Rendering (UI, game renderer, effects)
|
||||
├── persistence/ # Score management
|
||||
├── states/ # State implementations
|
||||
└── utils/ # Utilities
|
||||
```
|
||||
This structure is logical and easy to navigate.
|
||||
|
||||
### 4. **Build System**
|
||||
- **CMake**: Modern CMake with proper target configuration
|
||||
- **vcpkg**: Excellent dependency management
|
||||
- **Cross-platform**: Support for Windows and macOS
|
||||
- **Testing**: Catch2 integration for unit tests
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Areas for Improvement
|
||||
|
||||
### 1. **Memory Management Issues**
|
||||
|
||||
#### **Problem: Raw Pointer Usage**
|
||||
**Location:** `MenuState.h`, `main.cpp`
|
||||
```cpp
|
||||
// MenuState.h (lines 17-21)
|
||||
SDL_Texture* playIcon = nullptr;
|
||||
SDL_Texture* levelIcon = nullptr;
|
||||
SDL_Texture* optionsIcon = nullptr;
|
||||
SDL_Texture* exitIcon = nullptr;
|
||||
```
|
||||
|
||||
**Issue:** Raw pointers to SDL resources without proper cleanup in all code paths.
|
||||
|
||||
**Recommendation:**
|
||||
```cpp
|
||||
// Create a smart pointer wrapper for SDL_Texture
|
||||
struct SDL_TextureDeleter {
|
||||
void operator()(SDL_Texture* tex) const {
|
||||
if (tex) SDL_DestroyTexture(tex);
|
||||
}
|
||||
};
|
||||
using SDL_TexturePtr = std::unique_ptr<SDL_Texture, SDL_TextureDeleter>;
|
||||
|
||||
// Usage in MenuState.h
|
||||
private:
|
||||
SDL_TexturePtr playIcon;
|
||||
SDL_TexturePtr levelIcon;
|
||||
SDL_TexturePtr optionsIcon;
|
||||
SDL_TexturePtr exitIcon;
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Automatic cleanup
|
||||
- Exception safety
|
||||
- No manual memory management
|
||||
- Clear ownership semantics
|
||||
|
||||
---
|
||||
|
||||
### 2. **Error Handling**
|
||||
|
||||
#### **Problem: Inconsistent Error Handling**
|
||||
**Location:** `main.cpp` (lines 86-114)
|
||||
```cpp
|
||||
static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::string& path, int* outW = nullptr, int* outH = nullptr) {
|
||||
if (!renderer) {
|
||||
return nullptr; // Silent failure
|
||||
}
|
||||
|
||||
SDL_Surface* surface = IMG_Load(resolvedPath.c_str());
|
||||
if (!surface) {
|
||||
SDL_LogError(...); // Logs but returns nullptr
|
||||
return nullptr;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- Silent failures make debugging difficult
|
||||
- Callers must check for `nullptr` (easy to forget)
|
||||
- No way to distinguish between different error types
|
||||
|
||||
**Recommendation:**
|
||||
```cpp
|
||||
#include <expected> // C++23, or use tl::expected for C++20
|
||||
|
||||
struct TextureLoadError {
|
||||
std::string message;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
std::expected<SDL_TexturePtr, TextureLoadError>
|
||||
loadTextureFromImage(SDL_Renderer* renderer, const std::string& path,
|
||||
int* outW = nullptr, int* outH = nullptr) {
|
||||
if (!renderer) {
|
||||
return std::unexpected(TextureLoadError{
|
||||
"Renderer is null", path
|
||||
});
|
||||
}
|
||||
|
||||
const std::string resolvedPath = AssetPath::resolveImagePath(path);
|
||||
SDL_Surface* surface = IMG_Load(resolvedPath.c_str());
|
||||
if (!surface) {
|
||||
return std::unexpected(TextureLoadError{
|
||||
std::string("Failed to load: ") + SDL_GetError(),
|
||||
resolvedPath
|
||||
});
|
||||
}
|
||||
|
||||
// ... success case
|
||||
return SDL_TexturePtr(texture);
|
||||
}
|
||||
|
||||
// Usage:
|
||||
auto result = loadTextureFromImage(renderer, "path.png");
|
||||
if (result) {
|
||||
// Use result.value()
|
||||
} else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to load %s: %s",
|
||||
result.error().path.c_str(),
|
||||
result.error().message.c_str());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **Code Duplication**
|
||||
|
||||
#### **Problem: Repeated Patterns**
|
||||
**Location:** `MenuState.cpp`, `PlayingState.cpp`, `OptionsState.cpp`
|
||||
|
||||
Similar lambda patterns for exit popup handling:
|
||||
```cpp
|
||||
auto setExitSelection = [&](int value) {
|
||||
if (ctx.exitPopupSelectedButton) {
|
||||
*ctx.exitPopupSelectedButton = value;
|
||||
}
|
||||
};
|
||||
|
||||
auto getExitSelection = [&]() -> int {
|
||||
return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
|
||||
};
|
||||
```
|
||||
|
||||
**Recommendation:**
|
||||
Create a helper class in `StateContext`:
|
||||
```cpp
|
||||
// StateContext.h
|
||||
class ExitPopupHelper {
|
||||
public:
|
||||
ExitPopupHelper(int* selectedButton, bool* showPopup)
|
||||
: m_selectedButton(selectedButton), m_showPopup(showPopup) {}
|
||||
|
||||
void setSelection(int value) {
|
||||
if (m_selectedButton) *m_selectedButton = value;
|
||||
}
|
||||
|
||||
int getSelection() const {
|
||||
return m_selectedButton ? *m_selectedButton : 1;
|
||||
}
|
||||
|
||||
void show() {
|
||||
if (m_showPopup) *m_showPopup = true;
|
||||
}
|
||||
|
||||
void hide() {
|
||||
if (m_showPopup) *m_showPopup = false;
|
||||
}
|
||||
|
||||
bool isVisible() const {
|
||||
return m_showPopup && *m_showPopup;
|
||||
}
|
||||
|
||||
private:
|
||||
int* m_selectedButton;
|
||||
bool* m_showPopup;
|
||||
};
|
||||
|
||||
// Usage in states:
|
||||
ExitPopupHelper exitPopup(ctx.exitPopupSelectedButton, ctx.showExitConfirmPopup);
|
||||
exitPopup.setSelection(0);
|
||||
if (exitPopup.isVisible()) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. **Magic Numbers**
|
||||
|
||||
#### **Problem: Some Magic Numbers Still Present**
|
||||
**Location:** `MenuState.cpp` (lines 269-273)
|
||||
```cpp
|
||||
float btnW = 200.0f; // Fixed width to match background buttons
|
||||
float btnH = 70.0f; // Fixed height to match background buttons
|
||||
float btnX = LOGICAL_W * 0.5f + contentOffsetX;
|
||||
float btnY = LOGICAL_H * 0.865f + contentOffsetY;
|
||||
```
|
||||
|
||||
**Recommendation:**
|
||||
Add to `Config.h`:
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. **File I/O for Debugging**
|
||||
|
||||
#### **Problem: Debug Logging to Files**
|
||||
**Location:** `MenuState.cpp` (lines 182-184, 195-203, etc.)
|
||||
```cpp
|
||||
FILE* f = fopen("tetris_trace.log", "a");
|
||||
if (f) {
|
||||
fprintf(f, "MenuState::render entry\n");
|
||||
fclose(f);
|
||||
}
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- File handles not checked properly
|
||||
- No error handling
|
||||
- Performance overhead in production
|
||||
- Should use proper logging framework
|
||||
|
||||
**Recommendation:**
|
||||
```cpp
|
||||
// Create a simple logger utility
|
||||
// src/utils/Logger.h
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
enum class Level { TRACE, DEBUG, INFO, WARN, ERROR };
|
||||
|
||||
static Logger& instance();
|
||||
|
||||
void setLevel(Level level) { m_level = level; }
|
||||
void setFile(const std::string& path);
|
||||
|
||||
template<typename... Args>
|
||||
void trace(const char* fmt, Args... args) {
|
||||
log(Level::TRACE, fmt, args...);
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
void debug(const char* fmt, Args... args) {
|
||||
log(Level::DEBUG, fmt, args...);
|
||||
}
|
||||
|
||||
private:
|
||||
Logger() = default;
|
||||
|
||||
template<typename... Args>
|
||||
void log(Level level, const char* fmt, Args... args);
|
||||
|
||||
Level m_level = Level::INFO;
|
||||
std::ofstream m_file;
|
||||
std::mutex m_mutex;
|
||||
};
|
||||
|
||||
// Usage:
|
||||
#ifdef DEBUG
|
||||
Logger::instance().trace("MenuState::render entry");
|
||||
#endif
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. **Const Correctness**
|
||||
|
||||
#### **Problem: Missing const in Some Places**
|
||||
**Location:** `StateContext` and various state methods
|
||||
|
||||
**Recommendation:**
|
||||
```cpp
|
||||
// State.h
|
||||
class State {
|
||||
public:
|
||||
virtual void render(SDL_Renderer* renderer, float logicalScale,
|
||||
SDL_Rect logicalVP) const = 0; // Add const
|
||||
// Render shouldn't modify state
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. **Thread Safety**
|
||||
|
||||
#### **Problem: Potential Race Conditions**
|
||||
**Location:** `Audio.cpp` - Background loading
|
||||
|
||||
**Current:**
|
||||
```cpp
|
||||
std::vector<AudioTrack> tracks;
|
||||
std::mutex tracksMutex;
|
||||
```
|
||||
|
||||
**Recommendation:**
|
||||
- Document thread safety guarantees
|
||||
- Use `std::shared_mutex` for read-heavy operations
|
||||
- Consider using lock-free data structures for performance-critical paths
|
||||
|
||||
```cpp
|
||||
// Audio.h
|
||||
class Audio {
|
||||
private:
|
||||
std::vector<AudioTrack> tracks;
|
||||
mutable std::shared_mutex tracksMutex; // Allow concurrent reads
|
||||
|
||||
public:
|
||||
// Read operation - shared lock
|
||||
int getLoadedTrackCount() const {
|
||||
std::shared_lock lock(tracksMutex);
|
||||
return tracks.size();
|
||||
}
|
||||
|
||||
// Write operation - exclusive lock
|
||||
void addTrack(const std::string& path) {
|
||||
std::unique_lock lock(tracksMutex);
|
||||
tracks.push_back(loadTrack(path));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. **Testing Coverage**
|
||||
|
||||
#### **Current State:**
|
||||
Only one test file: `tests/GravityTests.cpp`
|
||||
|
||||
**Recommendation:**
|
||||
Add comprehensive tests:
|
||||
```
|
||||
tests/
|
||||
├── GravityTests.cpp ✅ Exists
|
||||
├── GameLogicTests.cpp ❌ Missing
|
||||
├── ScoreManagerTests.cpp ❌ Missing
|
||||
├── StateTransitionTests.cpp ❌ Missing
|
||||
└── AudioSystemTests.cpp ❌ Missing
|
||||
```
|
||||
|
||||
**Example Test Structure:**
|
||||
```cpp
|
||||
// tests/GameLogicTests.cpp
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "gameplay/core/Game.h"
|
||||
|
||||
TEST_CASE("Game initialization", "[game]") {
|
||||
Game game(0);
|
||||
|
||||
SECTION("Board starts empty") {
|
||||
const auto& board = game.boardRef();
|
||||
REQUIRE(std::all_of(board.begin(), board.end(),
|
||||
[](int cell) { return cell == 0; }));
|
||||
}
|
||||
|
||||
SECTION("Score starts at zero") {
|
||||
REQUIRE(game.score() == 0);
|
||||
REQUIRE(game.lines() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Piece rotation", "[game]") {
|
||||
Game game(0);
|
||||
|
||||
SECTION("Clockwise rotation") {
|
||||
auto initialRot = game.current().rot;
|
||||
game.rotate(1);
|
||||
REQUIRE(game.current().rot == (initialRot + 1) % 4);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Line clearing", "[game]") {
|
||||
Game game(0);
|
||||
|
||||
SECTION("Single line clear awards correct score") {
|
||||
// Setup: Fill bottom row except one cell
|
||||
// ... test implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. **Documentation**
|
||||
|
||||
#### **Current State:**
|
||||
- Good inline comments
|
||||
- Config.h has excellent documentation
|
||||
- Missing: API documentation, architecture overview
|
||||
|
||||
**Recommendation:**
|
||||
Add Doxygen-style comments:
|
||||
```cpp
|
||||
/**
|
||||
* @class Game
|
||||
* @brief Core Tetris game logic engine
|
||||
*
|
||||
* Manages the game board, piece spawning, collision detection,
|
||||
* line clearing, and scoring. This class is independent of
|
||||
* rendering and input handling.
|
||||
*
|
||||
* @note Thread-safe for read operations, but write operations
|
||||
* (move, rotate, etc.) should only be called from the
|
||||
* main game thread.
|
||||
*
|
||||
* Example usage:
|
||||
* @code
|
||||
* Game game(5); // Start at level 5
|
||||
* game.tickGravity(16.67); // Update for one frame
|
||||
* if (game.isGameOver()) {
|
||||
* // Handle game over
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
class Game {
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Create `docs/ARCHITECTURE.md`:
|
||||
```markdown
|
||||
# Architecture Overview
|
||||
|
||||
## State Machine
|
||||
[Diagram of state transitions]
|
||||
|
||||
## Data Flow
|
||||
[Diagram showing how data flows through the system]
|
||||
|
||||
## Threading Model
|
||||
- Main thread: Rendering, input, game logic
|
||||
- Background thread: Audio loading
|
||||
- Audio callback thread: Audio mixing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. **Performance Considerations**
|
||||
|
||||
#### **Issue: Frequent String Allocations**
|
||||
**Location:** Various places using `std::string` for paths
|
||||
|
||||
**Recommendation:**
|
||||
```cpp
|
||||
// Use string_view for read-only string parameters
|
||||
#include <string_view>
|
||||
|
||||
SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer,
|
||||
std::string_view path, // Changed
|
||||
int* outW = nullptr,
|
||||
int* outH = nullptr);
|
||||
|
||||
// For compile-time strings, use constexpr
|
||||
namespace AssetPaths {
|
||||
constexpr std::string_view LOGO = "assets/images/logo.bmp";
|
||||
constexpr std::string_view BACKGROUND = "assets/images/main_background.bmp";
|
||||
}
|
||||
```
|
||||
|
||||
#### **Issue: Vector Reallocations**
|
||||
**Location:** `fireworks` vector in `main.cpp`
|
||||
|
||||
**Recommendation:**
|
||||
```cpp
|
||||
// Reserve capacity upfront
|
||||
fireworks.reserve(5); // Max 5 fireworks at once
|
||||
|
||||
// Or use a fixed-size container
|
||||
std::array<std::optional<TetrisFirework>, 5> fireworks;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Specific Recommendations by Priority
|
||||
|
||||
### **High Priority** (Do These First)
|
||||
|
||||
1. **Replace raw SDL pointers with smart pointers**
|
||||
- Impact: Prevents memory leaks
|
||||
- Effort: Medium
|
||||
- Files: `MenuState.h`, `main.cpp`, all state files
|
||||
|
||||
2. **Remove debug file I/O from production code**
|
||||
- Impact: Performance, code cleanliness
|
||||
- Effort: Low
|
||||
- Files: `MenuState.cpp`, `main.cpp`
|
||||
|
||||
3. **Add error handling to asset loading**
|
||||
- Impact: Better debugging, crash prevention
|
||||
- Effort: Medium
|
||||
- Files: `main.cpp`, `AssetManager.cpp`
|
||||
|
||||
### **Medium Priority**
|
||||
|
||||
4. **Extract common patterns into helper classes**
|
||||
- Impact: Code maintainability
|
||||
- Effort: Medium
|
||||
- Files: All state files
|
||||
|
||||
5. **Move remaining magic numbers to Config.h**
|
||||
- Impact: Maintainability
|
||||
- Effort: Low
|
||||
- Files: `MenuState.cpp`, `UIRenderer.cpp`
|
||||
|
||||
6. **Add comprehensive unit tests**
|
||||
- Impact: Code quality, regression prevention
|
||||
- Effort: High
|
||||
- Files: New test files
|
||||
|
||||
### **Low Priority** (Nice to Have)
|
||||
|
||||
7. **Add Doxygen documentation**
|
||||
- Impact: Developer onboarding
|
||||
- Effort: Medium
|
||||
|
||||
8. **Performance profiling and optimization**
|
||||
- Impact: Depends on current performance
|
||||
- Effort: Medium
|
||||
|
||||
9. **Consider using `std::expected` for error handling**
|
||||
- Impact: Better error handling
|
||||
- Effort: High (requires C++23 or external library)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Code Style Observations
|
||||
|
||||
### **Good Practices You're Already Following:**
|
||||
|
||||
✅ **Consistent naming conventions:**
|
||||
- Classes: `PascalCase` (e.g., `MenuState`, `GameRenderer`)
|
||||
- Functions: `camelCase` (e.g., `tickGravity`, `loadTexture`)
|
||||
- Constants: `UPPER_SNAKE_CASE` (e.g., `LOGICAL_W`, `DAS_DELAY`)
|
||||
- Member variables: `camelCase` with `m_` prefix in some places
|
||||
|
||||
✅ **Header guards:** Using `#pragma once`
|
||||
|
||||
✅ **Forward declarations:** Minimizing include dependencies
|
||||
|
||||
✅ **RAII:** Resources tied to object lifetime
|
||||
|
||||
### **Minor Style Inconsistencies:**
|
||||
|
||||
❌ **Inconsistent member variable naming:**
|
||||
```cpp
|
||||
// Some classes use m_ prefix
|
||||
float m_masterVolume = 1.0f;
|
||||
|
||||
// Others don't
|
||||
int selectedButton = 0;
|
||||
```
|
||||
|
||||
**Recommendation:** Pick one style and stick to it. I suggest:
|
||||
```cpp
|
||||
// Private members: m_ prefix
|
||||
float m_masterVolume = 1.0f;
|
||||
int m_selectedButton = 0;
|
||||
|
||||
// Public members: no prefix (rare in good design)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Architecture Suggestions
|
||||
|
||||
### **Consider Implementing:**
|
||||
|
||||
1. **Event System**
|
||||
Instead of callbacks, use an event bus:
|
||||
```cpp
|
||||
// events/GameEvents.h
|
||||
struct LineClearedEvent {
|
||||
int linesCleared;
|
||||
int newScore;
|
||||
};
|
||||
|
||||
struct LevelUpEvent {
|
||||
int newLevel;
|
||||
};
|
||||
|
||||
// EventBus.h
|
||||
class EventBus {
|
||||
public:
|
||||
template<typename Event>
|
||||
void subscribe(std::function<void(const Event&)> handler);
|
||||
|
||||
template<typename Event>
|
||||
void publish(const Event& event);
|
||||
};
|
||||
|
||||
// Usage in Game.cpp
|
||||
eventBus.publish(LineClearedEvent{linesCleared, _score});
|
||||
|
||||
// Usage in Audio system
|
||||
eventBus.subscribe<LineClearedEvent>([](const auto& e) {
|
||||
playLineClearSound(e.linesCleared);
|
||||
});
|
||||
```
|
||||
|
||||
2. **Component-Based UI**
|
||||
Extract UI components:
|
||||
```cpp
|
||||
class Button {
|
||||
public:
|
||||
void render(SDL_Renderer* renderer);
|
||||
bool isHovered(int mouseX, int mouseY) const;
|
||||
void onClick(std::function<void()> callback);
|
||||
};
|
||||
|
||||
class Panel {
|
||||
std::vector<std::unique_ptr<UIComponent>> children;
|
||||
};
|
||||
```
|
||||
|
||||
3. **Asset Manager**
|
||||
Centralize asset loading:
|
||||
```cpp
|
||||
class AssetManager {
|
||||
public:
|
||||
SDL_TexturePtr getTexture(std::string_view name);
|
||||
FontAtlas* getFont(std::string_view name);
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, SDL_TexturePtr> textures;
|
||||
std::unordered_map<std::string, std::unique_ptr<FontAtlas>> fonts;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Security Considerations
|
||||
|
||||
1. **File Path Validation**
|
||||
```cpp
|
||||
// AssetPath::resolveImagePath should validate paths
|
||||
// to prevent directory traversal attacks
|
||||
std::string resolveImagePath(std::string_view path) {
|
||||
// Reject paths with ".."
|
||||
if (path.find("..") != std::string_view::npos) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Invalid path: %s", path.data());
|
||||
return "";
|
||||
}
|
||||
// ... rest of implementation
|
||||
}
|
||||
```
|
||||
|
||||
2. **Score File Tampering**
|
||||
Consider adding checksums to score files:
|
||||
```cpp
|
||||
// Scores.cpp
|
||||
void ScoreManager::save() const {
|
||||
nlohmann::json j;
|
||||
j["scores"] = scores;
|
||||
j["checksum"] = computeChecksum(scores);
|
||||
// ... save to file
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Metrics
|
||||
|
||||
Based on the codebase analysis:
|
||||
|
||||
| Metric | Value | Rating |
|
||||
|--------|-------|--------|
|
||||
| **Code Organization** | Excellent | ⭐⭐⭐⭐⭐ |
|
||||
| **Modern C++ Usage** | Very Good | ⭐⭐⭐⭐ |
|
||||
| **Error Handling** | Fair | ⭐⭐⭐ |
|
||||
| **Memory Safety** | Good | ⭐⭐⭐⭐ |
|
||||
| **Test Coverage** | Poor | ⭐ |
|
||||
| **Documentation** | Good | ⭐⭐⭐⭐ |
|
||||
| **Performance** | Good | ⭐⭐⭐⭐ |
|
||||
| **Maintainability** | Very Good | ⭐⭐⭐⭐ |
|
||||
|
||||
**Overall Score: 4/5 ⭐⭐⭐⭐**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Wins (Easy Improvements)
|
||||
|
||||
1. **Add `.clang-format` file** for consistent formatting
|
||||
2. **Create `CONTRIBUTING.md`** with coding guidelines
|
||||
3. **Add pre-commit hooks** for formatting and linting
|
||||
4. **Set up GitHub Actions** for CI/CD
|
||||
5. **Add `README.md`** with build instructions and screenshots
|
||||
|
||||
---
|
||||
|
||||
## 📚 Recommended Resources
|
||||
|
||||
- **Modern C++ Best Practices:** https://isocpp.github.io/CppCoreGuidelines/
|
||||
- **SDL3 Migration Guide:** https://wiki.libsdl.org/SDL3/README/migration
|
||||
- **Game Programming Patterns:** https://gameprogrammingpatterns.com/
|
||||
- **C++ Testing with Catch2:** https://github.com/catchorg/Catch2
|
||||
|
||||
---
|
||||
|
||||
## ✅ Conclusion
|
||||
|
||||
Your Tetris project demonstrates **strong software engineering practices** with a clean architecture, modern C++ usage, and good separation of concerns. The main areas for improvement are:
|
||||
|
||||
1. Enhanced error handling
|
||||
2. Increased test coverage
|
||||
3. Elimination of raw pointers
|
||||
4. Removal of debug code from production
|
||||
|
||||
With these improvements, this codebase would be **production-ready** and serve as an excellent example of modern C++ game development.
|
||||
|
||||
**Keep up the excellent work!** 🎮
|
||||
363
IMPROVEMENTS_CHECKLIST.md
Normal file
@ -0,0 +1,363 @@
|
||||
# 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
|
||||
774
QUICK_START_IMPROVEMENTS.md
Normal file
@ -0,0 +1,774 @@
|
||||
# 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/fonts/Exo2.ttf
Normal file
BIN
assets/fonts/Orbitron.ttf
Normal file
BIN
assets/images/earth_back.jpg
Normal file
|
After Width: | Height: | Size: 513 KiB |
BIN
assets/images/levels/level0.jpg
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
assets/images/levels/level1.jpg
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
assets/images/levels/level10.jpg
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
assets/images/levels/level11.jpg
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
assets/images/levels/level12.jpg
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
assets/images/levels/level13.jpg
Normal file
|
After Width: | Height: | Size: 333 KiB |
BIN
assets/images/levels/level14.jpg
Normal file
|
After Width: | Height: | Size: 265 KiB |
BIN
assets/images/levels/level15.jpg
Normal file
|
After Width: | Height: | Size: 298 KiB |
BIN
assets/images/levels/level16.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
assets/images/levels/level17.jpg
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
assets/images/levels/level18.jpg
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
assets/images/levels/level19.jpg
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
assets/images/levels/level2.jpg
Normal file
|
After Width: | Height: | Size: 450 KiB |
BIN
assets/images/levels/level20.jpg
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
assets/images/levels/level21.jpg
Normal file
|
After Width: | Height: | Size: 322 KiB |
BIN
assets/images/levels/level22.jpg
Normal file
|
After Width: | Height: | Size: 323 KiB |
BIN
assets/images/levels/level23.jpg
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
assets/images/levels/level24.jpg
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
assets/images/levels/level25.jpg
Normal file
|
After Width: | Height: | Size: 387 KiB |
BIN
assets/images/levels/level26.jpg
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
assets/images/levels/level27.jpg
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
assets/images/levels/level28.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
assets/images/levels/level29.jpg
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
assets/images/levels/level3.jpg
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
assets/images/levels/level30.jpg
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
assets/images/levels/level31.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
assets/images/levels/level32.jpg
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
assets/images/levels/level4.jpg
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
assets/images/levels/level5.jpg
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
assets/images/levels/level6.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
assets/images/levels/level7.jpg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
assets/images/levels/level8.jpg
Normal file
|
After Width: | Height: | Size: 335 KiB |
BIN
assets/images/levels/level9.jpg
Normal file
|
After Width: | Height: | Size: 420 KiB |
|
Before Width: | Height: | Size: 930 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 24 KiB |
BIN
assets/images/main_screen.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
assets/images/next_panel.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
BIN
assets/images/panel_score.png
Normal file
|
After Width: | Height: | Size: 275 KiB |
BIN
assets/images/spacetris.png
Normal file
|
After Width: | Height: | Size: 486 KiB |
BIN
assets/images/statistics_panel.png
Normal file
|
After Width: | Height: | Size: 440 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 756 KiB |
|
Before Width: | Height: | Size: 1007 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 957 KiB |
|
Before Width: | Height: | Size: 805 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 947 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 825 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 970 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 1007 KiB |
|
Before Width: | Height: | Size: 638 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 828 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1010 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 995 KiB |
@ -12,10 +12,7 @@ void menu_updateFireworks(double frameMs);
|
||||
double menu_getLogoAnimCounter();
|
||||
int menu_getHoveredButton();
|
||||
|
||||
void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
|
||||
const std::string& label, bool isHovered, bool isSelected);
|
||||
|
||||
void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
|
||||
const std::string& label, SDL_Color bgColor, SDL_Color borderColor);
|
||||
|
||||
void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);
|
||||
// Legacy wrappers removed
|
||||
// void menu_drawEnhancedButton(...);
|
||||
// void menu_drawMenuButton(...);
|
||||
// void menu_drawSettingsPopup(...);
|
||||
|
||||
@ -32,7 +32,7 @@ namespace Config {
|
||||
constexpr int MAX_LEVELS = 20; // Maximum selectable starting level
|
||||
|
||||
// Gravity speed multiplier: 1.0 = normal, 2.0 = 2x slower, 0.5 = 2x faster
|
||||
constexpr double GRAVITY_SPEED_MULTIPLIER = 1;
|
||||
constexpr double GRAVITY_SPEED_MULTIPLIER = 2; // increase drop interval by ~100% to slow gravity
|
||||
}
|
||||
|
||||
// UI Layout constants
|
||||
|
||||
@ -1131,6 +1131,9 @@ void ApplicationManager::setupStateHandlers() {
|
||||
m_stateContext.pixelFont,
|
||||
m_stateContext.lineEffect,
|
||||
m_stateContext.blocksTex,
|
||||
m_stateContext.statisticsPanelTex,
|
||||
m_stateContext.scorePanelTex,
|
||||
m_stateContext.nextPanelTex,
|
||||
LOGICAL_W,
|
||||
LOGICAL_H,
|
||||
logicalScale,
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#include "Game.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
// Piece rotation bitmasks (row-major 4x4). Bit 0 = (0,0).
|
||||
@ -54,6 +55,10 @@ void Game::reset(int startLevel_) {
|
||||
std::fill(blockCounts.begin(), blockCounts.end(), 0);
|
||||
bag.clear();
|
||||
_score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_;
|
||||
_tetrisesMade = 0;
|
||||
_currentCombo = 0;
|
||||
_maxCombo = 0;
|
||||
_comboCount = 0;
|
||||
// Initialize gravity using NES timing table (ms per cell by level)
|
||||
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
|
||||
fallAcc = 0; gameOver=false; paused=false;
|
||||
@ -217,6 +222,19 @@ void Game::lockPiece() {
|
||||
// Update total lines
|
||||
_lines += cleared;
|
||||
|
||||
// Update combo counters: consecutive clears increase combo; reset when no clear
|
||||
_currentCombo += 1;
|
||||
if (_currentCombo > _maxCombo) _maxCombo = _currentCombo;
|
||||
// Count combos as any single clear that removes more than 1 line
|
||||
if (cleared > 1) {
|
||||
_comboCount += 1;
|
||||
}
|
||||
|
||||
// Track tetrises made
|
||||
if (cleared == 4) {
|
||||
_tetrisesMade += 1;
|
||||
}
|
||||
|
||||
// JS level progression (NES-like) using starting level rules
|
||||
// Both startLevel and _level are 0-based now.
|
||||
int targetLevel = startLevel;
|
||||
@ -241,7 +259,10 @@ void Game::lockPiece() {
|
||||
soundCallback(cleared);
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
// No clear -> reset combo
|
||||
_currentCombo = 0;
|
||||
}
|
||||
if (!gameOver) spawn();
|
||||
}
|
||||
|
||||
@ -400,8 +421,28 @@ void Game::rotate(int dir) {
|
||||
|
||||
// Try rotation at current position first
|
||||
if (!collides(p)) {
|
||||
cur = p;
|
||||
return;
|
||||
// If rotation at current position would place cells above the top,
|
||||
// attempt to shift it down so the topmost block sits at gy == 0.
|
||||
int minGy = std::numeric_limits<int>::max();
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (!cellFilled(p, cx, cy)) continue;
|
||||
minGy = std::min(minGy, p.y + cy);
|
||||
}
|
||||
}
|
||||
|
||||
if (minGy < 0) {
|
||||
Piece adj = p;
|
||||
adj.y += -minGy;
|
||||
if (!collides(adj)) {
|
||||
cur = adj;
|
||||
return;
|
||||
}
|
||||
// Can't shift into place without collision - fall through to kicks
|
||||
} else {
|
||||
cur = p;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Standard SRS Wall Kicks
|
||||
@ -457,8 +498,30 @@ void Game::rotate(int dir) {
|
||||
test.x = cur.x + kick.first;
|
||||
test.y = cur.y + kick.second;
|
||||
if (!collides(test)) {
|
||||
cur = test;
|
||||
return;
|
||||
// Prevent rotated piece from ending up above the visible playfield.
|
||||
// If any cell of `test` is above the top (gy < 0), try shifting it
|
||||
// downward so the highest block sits at row 0. Accept the shift
|
||||
// only if it doesn't collide; otherwise keep searching kicks.
|
||||
int minGy = std::numeric_limits<int>::max();
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (!cellFilled(test, cx, cy)) continue;
|
||||
minGy = std::min(minGy, test.y + cy);
|
||||
}
|
||||
}
|
||||
|
||||
if (minGy < 0) {
|
||||
Piece adj = test;
|
||||
adj.y += -minGy; // shift down so topmost block is at gy == 0
|
||||
if (!collides(adj)) {
|
||||
cur = adj;
|
||||
return;
|
||||
}
|
||||
// couldn't shift without collision, try next kick
|
||||
} else {
|
||||
cur = test;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,6 +81,10 @@ public:
|
||||
const std::vector<SDL_Point>& getHardDropCells() const { return hardDropCells; }
|
||||
uint32_t getHardDropFxId() const { return hardDropFxId; }
|
||||
uint64_t getCurrentPieceSequence() const { return pieceSequence; }
|
||||
// Additional stats
|
||||
int tetrisesMade() const { return _tetrisesMade; }
|
||||
int maxCombo() const { return _maxCombo; }
|
||||
int comboCount() const { return _comboCount; }
|
||||
|
||||
private:
|
||||
std::array<int, COLS*ROWS> board{}; // 0 empty else color index
|
||||
@ -94,6 +98,10 @@ private:
|
||||
int _score{0};
|
||||
int _lines{0};
|
||||
int _level{1};
|
||||
int _tetrisesMade{0};
|
||||
int _currentCombo{0};
|
||||
int _maxCombo{0};
|
||||
int _comboCount{0};
|
||||
double gravityMs{800.0};
|
||||
double fallAcc{0.0};
|
||||
Uint64 _startTime{0}; // Performance counter at game start
|
||||
|
||||
@ -122,6 +122,9 @@ void GameRenderer::renderPlayingState(
|
||||
FontAtlas* pixelFont,
|
||||
LineEffect* lineEffect,
|
||||
SDL_Texture* blocksTex,
|
||||
SDL_Texture* statisticsPanelTex,
|
||||
SDL_Texture* scorePanelTex,
|
||||
SDL_Texture* nextPanelTex,
|
||||
float logicalW,
|
||||
float logicalH,
|
||||
float logicalScale,
|
||||
@ -198,10 +201,19 @@ void GameRenderer::renderPlayingState(
|
||||
lineEffect->startLineClear(completedLines, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
||||
}
|
||||
|
||||
// Draw game grid border
|
||||
drawRectWithOffset(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255});
|
||||
drawRectWithOffset(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 255});
|
||||
drawRectWithOffset(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255});
|
||||
// Draw styled game grid border and semi-transparent background so the scene shows through.
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// Outer glow layers (subtle, increasing spread, decreasing alpha)
|
||||
drawRectWithOffset(gridX - 8 - contentOffsetX, gridY - 8 - contentOffsetY, GRID_W + 16, GRID_H + 16, {100, 120, 200, 28});
|
||||
drawRectWithOffset(gridX - 6 - contentOffsetX, gridY - 6 - contentOffsetY, GRID_W + 12, GRID_H + 12, {100, 120, 200, 40});
|
||||
|
||||
// Accent border (brighter, thin)
|
||||
drawRectWithOffset(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 220});
|
||||
drawRectWithOffset(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 200});
|
||||
|
||||
// Do NOT fill the interior of the grid so the background shows through.
|
||||
// (Intentionally leave the playfield interior transparent.)
|
||||
|
||||
// Draw panel backgrounds
|
||||
SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160);
|
||||
@ -211,7 +223,7 @@ void GameRenderer::renderPlayingState(
|
||||
SDL_FRect rbg{scoreX - 16, gridY - 16, statsW + 32, GRID_H + 32};
|
||||
SDL_RenderFillRect(renderer, &rbg);
|
||||
|
||||
// Draw grid lines
|
||||
// Draw grid lines (solid so grid remains legible over background)
|
||||
SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255);
|
||||
for (int x = 1; x < Game::COLS; ++x) {
|
||||
float lineX = gridX + x * finalBlockSize;
|
||||
@ -233,8 +245,16 @@ void GameRenderer::renderPlayingState(
|
||||
drawRectWithOffset(statsX - contentOffsetX, statsY - contentOffsetY, statsW, statsH, {30, 35, 50, 255});
|
||||
|
||||
// Draw next piece preview panel border
|
||||
drawRectWithOffset(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255});
|
||||
drawRectWithOffset(nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH, {30, 35, 50, 255});
|
||||
// If a NEXT panel texture was provided, draw it instead of the custom
|
||||
// background/outline. The texture will be scaled to fit the panel area.
|
||||
if (nextPanelTex) {
|
||||
SDL_FRect dst{ nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH };
|
||||
SDL_SetTextureBlendMode(nextPanelTex, SDL_BLENDMODE_BLEND);
|
||||
SDL_RenderTexture(renderer, nextPanelTex, nullptr, &dst);
|
||||
} else {
|
||||
drawRectWithOffset(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255});
|
||||
drawRectWithOffset(nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH, {30, 35, 50, 255});
|
||||
}
|
||||
|
||||
// Draw the game board
|
||||
const auto &board = game->boardRef();
|
||||
@ -297,7 +317,8 @@ void GameRenderer::renderPlayingState(
|
||||
// Draw next piece preview
|
||||
pixelFont->draw(renderer, nextX + 10, nextY - 20, "NEXT", 1.0f, {255, 220, 0, 255});
|
||||
if (game->next().type < PIECE_COUNT) {
|
||||
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(game->next().type), nextX + 10, nextY + 10, finalBlockSize * 0.6f);
|
||||
// Nudge preview slightly upward to remove thin bottom artifact
|
||||
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(game->next().type), nextX + 10, nextY + 5, finalBlockSize * 0.6f);
|
||||
}
|
||||
|
||||
// Draw block statistics (left panel)
|
||||
@ -339,14 +360,20 @@ void GameRenderer::renderPlayingState(
|
||||
snprintf(countStr, sizeof(countStr), "%d", count);
|
||||
int countW = 0, countH = 0;
|
||||
pixelFont->measure(countStr, 1.0f, countW, countH);
|
||||
float countX = previewX + rowWidth - static_cast<float>(countW);
|
||||
// Horizontal shift to push the counts/percent a bit more to the right
|
||||
const float statsNumbersShift = 20.0f;
|
||||
// Small left shift for progress bar so the track aligns better with the design
|
||||
const float statsBarShift = -10.0f;
|
||||
float countX = previewX + rowWidth - static_cast<float>(countW) + statsNumbersShift;
|
||||
float countY = previewY + 9.0f;
|
||||
|
||||
int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(count) / double(totalBlocks))) : 0;
|
||||
char percStr[16];
|
||||
snprintf(percStr, sizeof(percStr), "%d%%", perc);
|
||||
int percW = 0, percH = 0;
|
||||
pixelFont->measure(percStr, 0.8f, percW, percH);
|
||||
|
||||
float barX = previewX;
|
||||
float barX = previewX + statsBarShift;
|
||||
float barY = previewY + pieceHeight + 12.0f;
|
||||
float barH = 6.0f;
|
||||
float barW = rowWidth;
|
||||
@ -369,7 +396,9 @@ void GameRenderer::renderPlayingState(
|
||||
|
||||
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(i), previewX, previewY, previewSize);
|
||||
pixelFont->draw(renderer, countX, countY, countStr, 1.0f, {245, 245, 255, 255});
|
||||
pixelFont->draw(renderer, previewX, percY, percStr, 0.8f, {215, 225, 240, 255});
|
||||
// Draw percent right-aligned near the same right edge as the count
|
||||
float percX = previewX + rowWidth - static_cast<float>(percW) + statsNumbersShift;
|
||||
pixelFont->draw(renderer, percX, percY, percStr, 0.8f, {215, 225, 240, 255});
|
||||
|
||||
SDL_SetRenderDrawColor(renderer, 110, 120, 140, 200);
|
||||
SDL_FRect track{barX, barY, barW, barH};
|
||||
|
||||
@ -20,8 +20,11 @@ public:
|
||||
Game* game,
|
||||
FontAtlas* pixelFont,
|
||||
LineEffect* lineEffect,
|
||||
SDL_Texture* blocksTex,
|
||||
float logicalW,
|
||||
SDL_Texture* blocksTex,
|
||||
SDL_Texture* statisticsPanelTex,
|
||||
SDL_Texture* scorePanelTex,
|
||||
SDL_Texture* nextPanelTex,
|
||||
float logicalW,
|
||||
float logicalH,
|
||||
float logicalScale,
|
||||
float winW,
|
||||
|
||||
365
src/graphics/effects/SpaceWarp.cpp
Normal file
@ -0,0 +1,365 @@
|
||||
#include "SpaceWarp.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
constexpr float MIN_ASPECT = 0.001f;
|
||||
}
|
||||
|
||||
SpaceWarp::SpaceWarp() {
|
||||
std::random_device rd;
|
||||
rng.seed(rd());
|
||||
setFlightMode(SpaceWarpFlightMode::Forward);
|
||||
}
|
||||
|
||||
void SpaceWarp::init(int w, int h, int starCount) {
|
||||
resize(w, h);
|
||||
stars.resize(std::max(8, starCount));
|
||||
for (auto& star : stars) {
|
||||
respawn(star, true);
|
||||
}
|
||||
comets.clear();
|
||||
cometSpawnTimer = randomRange(settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
|
||||
}
|
||||
|
||||
void SpaceWarp::resize(int w, int h) {
|
||||
width = std::max(1, w);
|
||||
height = std::max(1, h);
|
||||
centerX = width * 0.5f;
|
||||
centerY = height * 0.5f;
|
||||
warpFactor = std::max(width, height) * settings.warpFactorScale;
|
||||
}
|
||||
|
||||
void SpaceWarp::setSettings(const SpaceWarpSettings& newSettings) {
|
||||
settings = newSettings;
|
||||
warpFactor = std::max(width, height) * settings.warpFactorScale;
|
||||
cometSpawnTimer = std::clamp(cometSpawnTimer, settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
|
||||
}
|
||||
|
||||
void SpaceWarp::setFlightMode(SpaceWarpFlightMode mode) {
|
||||
flightMode = mode;
|
||||
autoPilotEnabled = false;
|
||||
switch (mode) {
|
||||
case SpaceWarpFlightMode::Forward:
|
||||
motion = {1.0f, 0.0f, 0.0f};
|
||||
break;
|
||||
case SpaceWarpFlightMode::BankLeft:
|
||||
motion = {1.05f, -0.85f, 0.0f};
|
||||
break;
|
||||
case SpaceWarpFlightMode::BankRight:
|
||||
motion = {1.05f, 0.85f, 0.0f};
|
||||
break;
|
||||
case SpaceWarpFlightMode::Reverse:
|
||||
motion = {-0.6f, 0.0f, 0.0f};
|
||||
break;
|
||||
case SpaceWarpFlightMode::Custom:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SpaceWarp::setFlightMotion(const SpaceWarpFlightMotion& newMotion) {
|
||||
motion = newMotion;
|
||||
flightMode = SpaceWarpFlightMode::Custom;
|
||||
autoPilotEnabled = false;
|
||||
}
|
||||
|
||||
void SpaceWarp::setAutoPilotEnabled(bool enabled) {
|
||||
autoPilotEnabled = enabled;
|
||||
if (enabled) {
|
||||
flightMode = SpaceWarpFlightMode::Custom;
|
||||
motionTarget = motion;
|
||||
autoTimer = 0.0f;
|
||||
scheduleNewAutoTarget();
|
||||
}
|
||||
}
|
||||
|
||||
void SpaceWarp::scheduleNewAutoTarget() {
|
||||
// Autopilot behavior:
|
||||
// - 90% of the time: gentle forward flight with small lateral/vertical drift
|
||||
// - 10% of the time: short lateral "bank" burst (stronger lateral speed) for a while
|
||||
float choice = randomRange(0.0f, 1.0f);
|
||||
if (choice < 0.90f) {
|
||||
// Normal forward flight
|
||||
motionTarget.forwardScale = randomRange(0.95f, 1.12f);
|
||||
motionTarget.lateralSpeed = randomRange(-0.18f, 0.18f);
|
||||
motionTarget.verticalSpeed = randomRange(-0.12f, 0.12f);
|
||||
// Longer interval between aggressive maneuvers
|
||||
autoTimer = randomRange(autoMinInterval, autoMaxInterval);
|
||||
} else {
|
||||
// Occasional lateral bank burst
|
||||
motionTarget.forwardScale = randomRange(0.90f, 1.10f);
|
||||
// Pick left or right burst
|
||||
float dir = (randomRange(0.0f, 1.0f) < 0.5f) ? -1.0f : 1.0f;
|
||||
motionTarget.lateralSpeed = dir * randomRange(0.70f, 1.35f);
|
||||
// Allow modest vertical bias during a bank
|
||||
motionTarget.verticalSpeed = randomRange(-0.35f, 0.35f);
|
||||
// Shorter duration for the burst so it feels like a brief maneuver
|
||||
autoTimer = randomRange(1.0f, 3.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void SpaceWarp::spawnComet() {
|
||||
WarpComet comet;
|
||||
float aspect = static_cast<float>(width) / static_cast<float>(std::max(1, height));
|
||||
float normalizedAspect = std::max(aspect, MIN_ASPECT);
|
||||
float xRange = settings.baseSpawnRange * 1.2f * (aspect >= 1.0f ? aspect : 1.0f);
|
||||
float yRange = settings.baseSpawnRange * 1.2f * (aspect >= 1.0f ? 1.0f : (1.0f / normalizedAspect));
|
||||
comet.x = randomRange(-xRange, xRange);
|
||||
comet.y = randomRange(-yRange, yRange);
|
||||
comet.z = randomRange(minDepth + 4.0f, maxDepth);
|
||||
float baseSpeed = randomRange(settings.minSpeed, settings.maxSpeed);
|
||||
float multiplier = randomRange(settings.cometSpeedMultiplierMin, settings.cometSpeedMultiplierMax);
|
||||
comet.speed = baseSpeed * multiplier;
|
||||
comet.size = randomRange(settings.cometMinSize, settings.cometMaxSize);
|
||||
comet.trailLength = randomRange(settings.cometMinTrail, settings.cometMaxTrail);
|
||||
comet.life = randomRange(1.8f, 3.4f);
|
||||
comet.maxLife = comet.life;
|
||||
float shade = randomRange(0.85f, 1.0f);
|
||||
Uint8 c = static_cast<Uint8>(std::clamp(220.0f + shade * 35.0f, 0.0f, 255.0f));
|
||||
comet.color = SDL_Color{c, Uint8(std::min(255.0f, c * 0.95f)), 255, 255};
|
||||
// Initialize screen positions based on projection so the comet is not stuck at center
|
||||
float sx = 0.0f, sy = 0.0f;
|
||||
if (projectPoint(comet.x, comet.y, comet.z, sx, sy)) {
|
||||
comet.screenX = sx;
|
||||
comet.screenY = sy;
|
||||
// Place prev slightly behind the head so the first frame shows motion/trail
|
||||
float jitter = std::max(4.0f, comet.trailLength * 0.08f);
|
||||
float ang = randomRange(0.0f, 6.28318530718f);
|
||||
comet.prevScreenX = comet.screenX - std::cos(ang) * jitter;
|
||||
comet.prevScreenY = comet.screenY - std::sin(ang) * jitter;
|
||||
} else {
|
||||
comet.prevScreenX = centerX;
|
||||
comet.prevScreenY = centerY;
|
||||
comet.screenX = centerX;
|
||||
comet.screenY = centerY;
|
||||
}
|
||||
comets.push_back(comet);
|
||||
}
|
||||
|
||||
float SpaceWarp::randomRange(float min, float max) {
|
||||
std::uniform_real_distribution<float> dist(min, max);
|
||||
return dist(rng);
|
||||
}
|
||||
|
||||
static int randomIntInclusive(std::mt19937& rng, int min, int max) {
|
||||
std::uniform_int_distribution<int> dist(min, max);
|
||||
return dist(rng);
|
||||
}
|
||||
|
||||
void SpaceWarp::respawn(WarpStar& star, bool randomDepth) {
|
||||
float aspect = static_cast<float>(width) / static_cast<float>(std::max(1, height));
|
||||
float normalizedAspect = std::max(aspect, MIN_ASPECT);
|
||||
float xRange = settings.baseSpawnRange * (aspect >= 1.0f ? aspect : 1.0f);
|
||||
float yRange = settings.baseSpawnRange * (aspect >= 1.0f ? 1.0f : (1.0f / normalizedAspect));
|
||||
star.x = randomRange(-xRange, xRange);
|
||||
star.y = randomRange(-yRange, yRange);
|
||||
star.z = randomDepth ? randomRange(minDepth, maxDepth) : maxDepth;
|
||||
star.speed = randomRange(settings.minSpeed, settings.maxSpeed);
|
||||
star.shade = randomRange(settings.minShade, settings.maxShade);
|
||||
static constexpr Uint8 GRAY_SHADES[] = {160, 180, 200, 220, 240};
|
||||
int idx = randomIntInclusive(rng, 0, int(std::size(GRAY_SHADES)) - 1);
|
||||
star.baseShade = GRAY_SHADES[idx];
|
||||
// Compute initial projected screen position so newly spawned stars aren't frozen at center
|
||||
float sx = 0.0f, sy = 0.0f;
|
||||
if (projectPoint(star.x, star.y, star.z, sx, sy)) {
|
||||
star.screenX = sx;
|
||||
star.screenY = sy;
|
||||
// give a small previous offset so trails and motion are visible immediately
|
||||
float jitter = std::max(1.0f, settings.maxTrailLength * 0.06f);
|
||||
float ang = randomRange(0.0f, 6.28318530718f);
|
||||
star.prevScreenX = star.screenX - std::cos(ang) * jitter;
|
||||
star.prevScreenY = star.screenY - std::sin(ang) * jitter;
|
||||
} else {
|
||||
star.prevScreenX = centerX;
|
||||
star.prevScreenY = centerY;
|
||||
star.screenX = centerX;
|
||||
star.screenY = centerY;
|
||||
}
|
||||
}
|
||||
|
||||
bool SpaceWarp::project(const WarpStar& star, float& outX, float& outY) const {
|
||||
return projectPoint(star.x, star.y, star.z, outX, outY);
|
||||
}
|
||||
|
||||
bool SpaceWarp::projectPoint(float x, float y, float z, float& outX, float& outY) const {
|
||||
if (z <= minDepth) {
|
||||
return false;
|
||||
}
|
||||
float perspective = warpFactor / (z + 0.001f);
|
||||
outX = centerX + x * perspective;
|
||||
outY = centerY + y * perspective;
|
||||
const float margin = settings.spawnMargin;
|
||||
return outX >= -margin && outX <= width + margin && outY >= -margin && outY <= height + margin;
|
||||
}
|
||||
|
||||
void SpaceWarp::update(float deltaSeconds) {
|
||||
if (stars.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.cometSpawnIntervalMax > 0.0f) {
|
||||
cometSpawnTimer -= deltaSeconds;
|
||||
if (cometSpawnTimer <= 0.0f) {
|
||||
spawnComet();
|
||||
cometSpawnTimer = randomRange(settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
|
||||
}
|
||||
}
|
||||
|
||||
if (autoPilotEnabled) {
|
||||
autoTimer -= deltaSeconds;
|
||||
if (autoTimer <= 0.0f) {
|
||||
scheduleNewAutoTarget();
|
||||
}
|
||||
auto follow = std::clamp(deltaSeconds * 0.45f, 0.0f, 1.0f);
|
||||
motion.forwardScale = std::lerp(motion.forwardScale, motionTarget.forwardScale, follow);
|
||||
motion.lateralSpeed = std::lerp(motion.lateralSpeed, motionTarget.lateralSpeed, follow);
|
||||
motion.verticalSpeed = std::lerp(motion.verticalSpeed, motionTarget.verticalSpeed, follow);
|
||||
}
|
||||
|
||||
const float forwardScale = (std::abs(motion.forwardScale) < 0.01f)
|
||||
? (motion.forwardScale >= 0.0f ? 0.01f : -0.01f)
|
||||
: motion.forwardScale;
|
||||
const bool movingBackward = forwardScale < 0.0f;
|
||||
const float lateralSpeed = motion.lateralSpeed;
|
||||
const float verticalSpeed = motion.verticalSpeed;
|
||||
|
||||
for (auto& star : stars) {
|
||||
star.z -= star.speed * deltaSeconds * forwardScale;
|
||||
if (!movingBackward) {
|
||||
if (star.z <= minDepth) {
|
||||
respawn(star, true);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (star.z >= maxDepth) {
|
||||
respawn(star, true);
|
||||
star.z = minDepth + randomRange(0.25f, 24.0f);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
float closeness = 1.0f - std::clamp(star.z / maxDepth, 0.0f, 1.0f);
|
||||
float driftScale = (0.35f + closeness * 1.25f);
|
||||
star.x += lateralSpeed * deltaSeconds * driftScale;
|
||||
star.y += verticalSpeed * deltaSeconds * driftScale;
|
||||
|
||||
float sx = 0.0f;
|
||||
float sy = 0.0f;
|
||||
if (!project(star, sx, sy)) {
|
||||
respawn(star, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
star.prevScreenX = star.screenX;
|
||||
star.prevScreenY = star.screenY;
|
||||
star.screenX = sx;
|
||||
star.screenY = sy;
|
||||
|
||||
float dx = star.screenX - star.prevScreenX;
|
||||
float dy = star.screenY - star.prevScreenY;
|
||||
float lenSq = dx * dx + dy * dy;
|
||||
float maxStreak = std::max(settings.maxTrailLength, 0.0f);
|
||||
if (maxStreak > 0.0f && lenSq > maxStreak * maxStreak) {
|
||||
float len = std::sqrt(lenSq);
|
||||
float scale = maxStreak / len;
|
||||
star.prevScreenX = star.screenX - dx * scale;
|
||||
star.prevScreenY = star.screenY - dy * scale;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = comets.begin(); it != comets.end();) {
|
||||
auto& comet = *it;
|
||||
comet.life -= deltaSeconds;
|
||||
comet.z -= comet.speed * deltaSeconds * forwardScale;
|
||||
bool expired = comet.life <= 0.0f;
|
||||
if (!movingBackward) {
|
||||
if (comet.z <= minDepth * 0.35f) expired = true;
|
||||
} else {
|
||||
if (comet.z >= maxDepth + 40.0f) expired = true;
|
||||
}
|
||||
|
||||
float closeness = 1.0f - std::clamp(comet.z / maxDepth, 0.0f, 1.0f);
|
||||
float driftScale = (0.45f + closeness * 1.6f);
|
||||
comet.x += lateralSpeed * deltaSeconds * driftScale;
|
||||
comet.y += verticalSpeed * deltaSeconds * driftScale;
|
||||
|
||||
float sx = 0.0f;
|
||||
float sy = 0.0f;
|
||||
if (!projectPoint(comet.x, comet.y, comet.z, sx, sy)) {
|
||||
expired = true;
|
||||
} else {
|
||||
comet.prevScreenX = comet.screenX;
|
||||
comet.prevScreenY = comet.screenY;
|
||||
comet.screenX = sx;
|
||||
comet.screenY = sy;
|
||||
|
||||
float dx = comet.screenX - comet.prevScreenX;
|
||||
float dy = comet.screenY - comet.prevScreenY;
|
||||
float lenSq = dx * dx + dy * dy;
|
||||
float maxTrail = std::max(comet.trailLength, 0.0f);
|
||||
if (maxTrail > 0.0f && lenSq > maxTrail * maxTrail) {
|
||||
float len = std::sqrt(lenSq);
|
||||
float scale = maxTrail / len;
|
||||
comet.prevScreenX = comet.screenX - dx * scale;
|
||||
comet.prevScreenY = comet.screenY - dy * scale;
|
||||
}
|
||||
}
|
||||
|
||||
if (expired) {
|
||||
it = comets.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) {
|
||||
if (stars.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_BlendMode previous = SDL_BLENDMODE_NONE;
|
||||
SDL_GetRenderDrawBlendMode(renderer, &previous);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD);
|
||||
|
||||
for (const auto& star : stars) {
|
||||
float depthFactor = 1.0f - std::clamp(star.z / maxDepth, 0.0f, 1.0f);
|
||||
float alphaBase = std::clamp(settings.minAlpha + depthFactor * settings.alphaDepthBoost, 0.0f, 255.0f);
|
||||
Uint8 alpha = static_cast<Uint8>(std::clamp(alphaBase * alphaScale, 0.0f, 255.0f));
|
||||
float colorValue = std::clamp(
|
||||
star.baseShade * (settings.baseShadeScale + depthFactor * settings.depthColorScale) * star.shade,
|
||||
settings.minColor,
|
||||
settings.maxColor);
|
||||
Uint8 color = static_cast<Uint8>(colorValue);
|
||||
|
||||
if (settings.drawTrails) {
|
||||
float trailAlphaFloat = alpha * settings.trailAlphaScale;
|
||||
Uint8 trailAlpha = static_cast<Uint8>(std::clamp(trailAlphaFloat, 0.0f, 255.0f));
|
||||
SDL_SetRenderDrawColor(renderer, color, color, color, trailAlpha);
|
||||
SDL_RenderLine(renderer, star.prevScreenX, star.prevScreenY, star.screenX, star.screenY);
|
||||
}
|
||||
|
||||
float dotSize = std::clamp(settings.minDotSize + depthFactor * (settings.maxDotSize - settings.minDotSize),
|
||||
settings.minDotSize,
|
||||
settings.maxDotSize);
|
||||
SDL_FRect dot{star.screenX - dotSize * 0.5f, star.screenY - dotSize * 0.5f, dotSize, dotSize};
|
||||
SDL_SetRenderDrawColor(renderer, color, color, color, alpha);
|
||||
SDL_RenderFillRect(renderer, &dot);
|
||||
}
|
||||
|
||||
for (const auto& comet : comets) {
|
||||
float lifeNorm = std::clamp(comet.life / comet.maxLife, 0.0f, 1.0f);
|
||||
Uint8 alpha = static_cast<Uint8>(std::clamp(220.0f * lifeNorm, 0.0f, 255.0f));
|
||||
SDL_SetRenderDrawColor(renderer, comet.color.r, comet.color.g, comet.color.b, alpha);
|
||||
SDL_RenderLine(renderer, comet.prevScreenX, comet.prevScreenY, comet.screenX, comet.screenY);
|
||||
|
||||
float size = comet.size * (0.8f + (1.0f - lifeNorm) * 0.6f);
|
||||
SDL_FRect head{comet.screenX - size * 0.5f, comet.screenY - size * 0.5f, size, size};
|
||||
SDL_RenderFillRect(renderer, &head);
|
||||
}
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, previous);
|
||||
}
|
||||
126
src/graphics/effects/SpaceWarp.h
Normal file
@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
struct SpaceWarpSettings {
|
||||
float baseSpawnRange = 1.45f; // logical radius for initial star positions
|
||||
float warpFactorScale = 122.85f; // scales perspective factor so stars spread faster or slower
|
||||
float spawnMargin = 60.0f; // how far offscreen a star can travel before respawn
|
||||
float minShade = 0.85f; // lower bound for per-star brightness multiplier
|
||||
float maxShade = 1.15f; // upper bound for per-star brightness multiplier
|
||||
float minSpeed = 120.0f; // slowest warp velocity (higher feels faster motion)
|
||||
float maxSpeed = 280.0f; // fastest warp velocity
|
||||
float minDotSize = 2.5f; // smallest star size in pixels
|
||||
float maxDotSize = 4.5f; // largest star size in pixels
|
||||
float minAlpha = 70.0f; // base opacity even for distant stars
|
||||
float alphaDepthBoost = 160.0f; // extra opacity applied as stars approach the camera
|
||||
float minColor = 180.0f; // clamp for minimum grayscale value
|
||||
float maxColor = 205.0f; // clamp for maximum grayscale value
|
||||
float baseShadeScale = 0.75f; // baseline multiplier applied to the sampled grayscale shade
|
||||
float depthColorScale = 0.55f; // how much depth affects the grayscale brightness
|
||||
bool drawTrails = true; // when true, also render streak lines for hyper-speed look
|
||||
float trailAlphaScale = 0.75f; // relative opacity for streak lines vs dots
|
||||
float maxTrailLength = 36.0f; // clamp length of each streak in pixels
|
||||
float cometSpawnIntervalMin = 2.8f; // minimum seconds between comet spawns
|
||||
float cometSpawnIntervalMax = 6.5f; // maximum seconds between comet spawns
|
||||
float cometSpeedMultiplierMin = 2.2f;// min multiplier for comet forward velocity
|
||||
float cometSpeedMultiplierMax = 4.5f;// max multiplier for comet forward velocity
|
||||
float cometMinTrail = 140.0f; // minimum comet trail length in pixels
|
||||
float cometMaxTrail = 280.0f; // maximum comet trail length in pixels
|
||||
float cometMinSize = 3.5f; // minimum comet head size
|
||||
float cometMaxSize = 6.5f; // maximum comet head size
|
||||
};
|
||||
|
||||
struct SpaceWarpFlightMotion {
|
||||
float forwardScale = 1.0f; // multiplier applied to each star's forward velocity (negative = backwards)
|
||||
float lateralSpeed = 0.0f; // normalized horizontal drift speed (left/right)
|
||||
float verticalSpeed = 0.0f; // normalized vertical drift speed (up/down)
|
||||
};
|
||||
|
||||
enum class SpaceWarpFlightMode {
|
||||
Forward = 0,
|
||||
BankLeft,
|
||||
BankRight,
|
||||
Reverse,
|
||||
Custom
|
||||
};
|
||||
|
||||
class SpaceWarp {
|
||||
public:
|
||||
SpaceWarp();
|
||||
void init(int width, int height, int starCount = 320);
|
||||
void resize(int width, int height);
|
||||
void update(float deltaSeconds);
|
||||
void draw(SDL_Renderer* renderer, float alphaScale = 1.0f);
|
||||
void setSettings(const SpaceWarpSettings& newSettings);
|
||||
const SpaceWarpSettings& getSettings() const { return settings; }
|
||||
void setFlightMode(SpaceWarpFlightMode mode);
|
||||
SpaceWarpFlightMode getFlightMode() const { return flightMode; }
|
||||
void setFlightMotion(const SpaceWarpFlightMotion& motion); // overrides mode with Custom
|
||||
const SpaceWarpFlightMotion& getFlightMotion() const { return motion; }
|
||||
void setAutoPilotEnabled(bool enabled);
|
||||
bool isAutoPilotEnabled() const { return autoPilotEnabled; }
|
||||
|
||||
private:
|
||||
struct WarpStar {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
float speed = 0.0f;
|
||||
float prevScreenX = 0.0f;
|
||||
float prevScreenY = 0.0f;
|
||||
float screenX = 0.0f;
|
||||
float screenY = 0.0f;
|
||||
float shade = 1.0f;
|
||||
Uint8 baseShade = 220;
|
||||
};
|
||||
|
||||
struct WarpComet {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
float speed = 0.0f;
|
||||
float life = 0.0f;
|
||||
float maxLife = 0.0f;
|
||||
float prevScreenX = 0.0f;
|
||||
float prevScreenY = 0.0f;
|
||||
float screenX = 0.0f;
|
||||
float screenY = 0.0f;
|
||||
float trailLength = 160.0f;
|
||||
float size = 4.0f;
|
||||
SDL_Color color{255, 255, 255, 255};
|
||||
};
|
||||
|
||||
void respawn(WarpStar& star, bool randomDepth = true);
|
||||
bool project(const WarpStar& star, float& outX, float& outY) const;
|
||||
bool projectPoint(float x, float y, float z, float& outX, float& outY) const;
|
||||
float randomRange(float min, float max);
|
||||
void spawnComet();
|
||||
|
||||
std::vector<WarpStar> stars;
|
||||
std::vector<WarpComet> comets;
|
||||
std::mt19937 rng;
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
float centerX = 0.0f;
|
||||
float centerY = 0.0f;
|
||||
float warpFactor = 520.0f;
|
||||
|
||||
SpaceWarpSettings settings{};
|
||||
SpaceWarpFlightMotion motion{};
|
||||
SpaceWarpFlightMode flightMode = SpaceWarpFlightMode::Forward;
|
||||
bool autoPilotEnabled = false;
|
||||
float autoTimer = 0.0f;
|
||||
float autoMinInterval = 3.5f;
|
||||
float autoMaxInterval = 7.5f;
|
||||
SpaceWarpFlightMotion motionTarget{};
|
||||
float cometSpawnTimer = 0.0f;
|
||||
|
||||
float minDepth = 2.0f;
|
||||
float maxDepth = 320.0f;
|
||||
|
||||
void scheduleNewAutoTarget();
|
||||
};
|
||||
@ -21,6 +21,9 @@ public:
|
||||
FontAtlas* pixelFont,
|
||||
LineEffect* lineEffect,
|
||||
SDL_Texture* blocksTex,
|
||||
SDL_Texture* statisticsPanelTex,
|
||||
SDL_Texture* scorePanelTex,
|
||||
SDL_Texture* nextPanelTex,
|
||||
float logicalW,
|
||||
float logicalH,
|
||||
float logicalScale,
|
||||
@ -47,11 +50,27 @@ public:
|
||||
int selectedButton
|
||||
);
|
||||
|
||||
// Public wrapper that forwards to the private tile-drawing helper. Use this if
|
||||
// calling from non-member helper functions (e.g. visual effects) that cannot
|
||||
// access private class members.
|
||||
static void drawBlockTexturePublic(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType);
|
||||
// Transport/teleport visual effect API (public): start a sci-fi "transport" animation
|
||||
// moving a visual copy of `piece` from screen pixel origin (startX,startY) to
|
||||
// target pixel origin (targetX,targetY). `tileSize` should be the same cell size
|
||||
// used for the grid. Duration is seconds.
|
||||
static void startTransportEffect(const Game::Piece& piece, float startX, float startY, float targetX, float targetY, float tileSize, float durationSeconds = 0.6f);
|
||||
// Convenience: compute the preview & grid positions using the same layout math
|
||||
// used by `renderPlayingState` and start the transport effect for the current
|
||||
// `game` using renderer layout parameters.
|
||||
static void startTransportEffectForGame(Game* game, SDL_Texture* blocksTex, float logicalW, float logicalH, float logicalScale, float winW, float winH, float durationSeconds = 0.6f);
|
||||
static bool isTransportActive();
|
||||
|
||||
private:
|
||||
// Helper functions for drawing game elements
|
||||
static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType);
|
||||
static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false, float pixelOffsetX = 0.0f, float pixelOffsetY = 0.0f);
|
||||
static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize);
|
||||
static void renderNextPanel(SDL_Renderer* renderer, FontAtlas* pixelFont, SDL_Texture* blocksTex, SDL_Texture* nextPanelTex, const Game::Piece& nextPiece, float panelX, float panelY, float panelW, float panelH, float tileSize);
|
||||
|
||||
// Helper function for drawing rectangles
|
||||
static void drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c);
|
||||
|
||||
202
src/graphics/renderers/UIRenderer.cpp
Normal file
@ -0,0 +1,202 @@
|
||||
#include "UIRenderer.h"
|
||||
#include "../ui/Font.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
void UIRenderer::drawSciFiPanel(SDL_Renderer* renderer, const SDL_FRect& rect, float alpha) {
|
||||
if (!renderer) return;
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
Uint8 alphaUint = static_cast<Uint8>(std::clamp(alpha * 255.0f, 0.0f, 255.0f));
|
||||
|
||||
// Drop shadow
|
||||
SDL_FRect shadow{rect.x + 6.0f, rect.y + 10.0f, rect.w, rect.h};
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, static_cast<Uint8>(120.0f * alpha));
|
||||
SDL_RenderFillRect(renderer, &shadow);
|
||||
|
||||
// Glow aura
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
SDL_FRect glow{rect.x - float(i * 2), rect.y - float(i * 2), rect.w + float(i * 4), rect.h + float(i * 4)};
|
||||
Uint8 glowAlpha = static_cast<Uint8>((42 - i * 8) * alpha);
|
||||
SDL_SetRenderDrawColor(renderer, 0, 180, 255, glowAlpha);
|
||||
SDL_RenderRect(renderer, &glow);
|
||||
}
|
||||
|
||||
// Body
|
||||
SDL_SetRenderDrawColor(renderer, 18, 30, 52, alphaUint);
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
|
||||
// Border
|
||||
SDL_SetRenderDrawColor(renderer, 70, 120, 210, alphaUint);
|
||||
SDL_RenderRect(renderer, &rect);
|
||||
}
|
||||
|
||||
void UIRenderer::drawButton(SDL_Renderer* renderer, FontAtlas* font, float cx, float cy, float w, float h,
|
||||
const std::string& label, bool isHovered, bool isSelected,
|
||||
SDL_Color bgColor, SDL_Color borderColor, bool textOnly, SDL_Texture* icon) {
|
||||
if (!renderer) return;
|
||||
|
||||
float x = cx - w * 0.5f;
|
||||
float y = cy - h * 0.5f;
|
||||
|
||||
if (!textOnly) {
|
||||
// Adjust colors based on state
|
||||
if (isSelected) {
|
||||
bgColor = {160, 190, 255, 255};
|
||||
SDL_SetRenderDrawColor(renderer, 255, 220, 0, 110);
|
||||
SDL_FRect glow{x - 10, y - 10, w + 20, h + 20};
|
||||
SDL_RenderFillRect(renderer, &glow);
|
||||
} else if (isHovered) {
|
||||
bgColor = {static_cast<Uint8>(std::min(255, bgColor.r + 40)),
|
||||
static_cast<Uint8>(std::min(255, bgColor.g + 40)),
|
||||
static_cast<Uint8>(std::min(255, bgColor.b + 40)),
|
||||
bgColor.a};
|
||||
}
|
||||
|
||||
// Neon glow aura around the button to increase visibility (subtle)
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
for (int gi = 0; gi < 3; ++gi) {
|
||||
float grow = 6.0f + gi * 3.0f;
|
||||
Uint8 glowA = static_cast<Uint8>(std::max(0, (int)borderColor.a / (3 - gi)));
|
||||
SDL_SetRenderDrawColor(renderer, borderColor.r, borderColor.g, borderColor.b, glowA);
|
||||
SDL_FRect glowRect{x - grow, y - grow, w + grow * 2.0f, h + grow * 2.0f};
|
||||
SDL_RenderRect(renderer, &glowRect);
|
||||
}
|
||||
|
||||
// Draw button background with border
|
||||
SDL_SetRenderDrawColor(renderer, borderColor.r, borderColor.g, borderColor.b, borderColor.a);
|
||||
SDL_FRect borderRect{x - 2, y - 2, w + 4, h + 4};
|
||||
SDL_RenderFillRect(renderer, &borderRect);
|
||||
|
||||
SDL_SetRenderDrawColor(renderer, bgColor.r, bgColor.g, bgColor.b, bgColor.a);
|
||||
SDL_FRect bgRect{x, y, w, h};
|
||||
SDL_RenderFillRect(renderer, &bgRect);
|
||||
}
|
||||
|
||||
// Draw icon if provided, otherwise draw text
|
||||
if (icon) {
|
||||
// Get icon dimensions
|
||||
float iconW = 0.0f, iconH = 0.0f;
|
||||
SDL_GetTextureSize(icon, &iconW, &iconH);
|
||||
|
||||
// Scale icon to fit nicely in button (60% of button height)
|
||||
float maxIconH = h * 0.6f;
|
||||
float scale = maxIconH / iconH;
|
||||
float scaledW = iconW * scale;
|
||||
float scaledH = iconH * scale;
|
||||
|
||||
// Center icon in button
|
||||
float iconX = cx - scaledW * 0.5f;
|
||||
float iconY = cy - scaledH * 0.5f;
|
||||
|
||||
// Apply yellow tint when selected
|
||||
if (isSelected) {
|
||||
SDL_SetTextureColorMod(icon, 255, 220, 0);
|
||||
} else {
|
||||
SDL_SetTextureColorMod(icon, 255, 255, 255);
|
||||
}
|
||||
|
||||
SDL_FRect iconRect{iconX, iconY, scaledW, scaledH};
|
||||
SDL_RenderTexture(renderer, icon, nullptr, &iconRect);
|
||||
|
||||
// Reset color mod
|
||||
SDL_SetTextureColorMod(icon, 255, 255, 255);
|
||||
} else if (font) {
|
||||
// Draw text (smaller scale for tighter buttons)
|
||||
float textScale = 1.2f;
|
||||
int textW = 0, textH = 0;
|
||||
font->measure(label, textScale, textW, textH);
|
||||
float tx = x + (w - static_cast<float>(textW)) * 0.5f;
|
||||
// Adjust vertical position for better alignment with background buttons
|
||||
// Vertically center text precisely within the button
|
||||
// Vertically center text precisely within the button, then nudge down slightly
|
||||
// to improve optical balance relative to icons and button art.
|
||||
const float textNudge = 3.0f; // tweak this value to move labels up/down
|
||||
float ty = y + (h - static_cast<float>(textH)) * 0.5f + textNudge;
|
||||
|
||||
// Choose text color based on selection state
|
||||
SDL_Color textColor = {255, 255, 255, 255}; // Default white
|
||||
if (isSelected) {
|
||||
textColor = {255, 220, 0, 255}; // Yellow when selected
|
||||
}
|
||||
|
||||
// Text shadow
|
||||
font->draw(renderer, tx + 2.0f, ty + 2.0f, label, textScale, {0, 0, 0, 200});
|
||||
// Text
|
||||
font->draw(renderer, tx, ty, label, textScale, textColor);
|
||||
}
|
||||
}
|
||||
|
||||
void UIRenderer::computeContentOffsets(float winW, float winH, float logicalW, float logicalH, float logicalScale, float& outOffsetX, float& outOffsetY) {
|
||||
float contentW = logicalW * logicalScale;
|
||||
float contentH = logicalH * logicalScale;
|
||||
outOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
||||
outOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
||||
}
|
||||
|
||||
void UIRenderer::drawLogo(SDL_Renderer* renderer, SDL_Texture* logoTex, float logicalW, float logicalH, float contentOffsetX, float contentOffsetY, int texW, int texH) {
|
||||
if (!renderer || !logoTex) return;
|
||||
|
||||
float w = 0.0f;
|
||||
float h = 0.0f;
|
||||
|
||||
if (texW > 0 && texH > 0) {
|
||||
w = static_cast<float>(texW);
|
||||
h = static_cast<float>(texH);
|
||||
} else {
|
||||
SDL_GetTextureSize(logoTex, &w, &h);
|
||||
}
|
||||
|
||||
if (w > 0.0f && h > 0.0f) {
|
||||
float maxWidth = logicalW * 0.6f;
|
||||
float scale = std::min(1.0f, maxWidth / w);
|
||||
float dw = w * scale;
|
||||
float dh = h * scale;
|
||||
float logoX = (logicalW - dw) * 0.5f + contentOffsetX;
|
||||
float logoY = logicalH * 0.05f + contentOffsetY;
|
||||
SDL_FRect dst{logoX, logoY, dw, dh};
|
||||
SDL_RenderTexture(renderer, logoTex, nullptr, &dst);
|
||||
}
|
||||
}
|
||||
|
||||
void UIRenderer::drawSettingsPopup(SDL_Renderer* renderer, FontAtlas* font, float logicalW, float logicalH, bool musicEnabled, bool soundEnabled) {
|
||||
if (!renderer || !font) return;
|
||||
|
||||
float popupW = 350, popupH = 260;
|
||||
float popupX = (logicalW - popupW) / 2;
|
||||
float popupY = (logicalH - popupH) / 2;
|
||||
|
||||
// Semi-transparent overlay
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 128);
|
||||
SDL_FRect overlay{0, 0, logicalW, logicalH};
|
||||
SDL_RenderFillRect(renderer, &overlay);
|
||||
|
||||
// Popup background
|
||||
SDL_SetRenderDrawColor(renderer, 100, 120, 160, 255);
|
||||
SDL_FRect bord{popupX-4, popupY-4, popupW+8, popupH+8};
|
||||
SDL_RenderFillRect(renderer, &bord);
|
||||
|
||||
SDL_SetRenderDrawColor(renderer, 40, 50, 70, 255);
|
||||
SDL_FRect body{popupX, popupY, popupW, popupH};
|
||||
SDL_RenderFillRect(renderer, &body);
|
||||
|
||||
// Title
|
||||
font->draw(renderer, popupX + 20, popupY + 20, "SETTINGS", 2.0f, {255, 220, 0, 255});
|
||||
|
||||
// Music toggle
|
||||
font->draw(renderer, popupX + 20, popupY + 70, "MUSIC:", 1.5f, {255, 255, 255, 255});
|
||||
const char* musicStatus = musicEnabled ? "ON" : "OFF";
|
||||
SDL_Color musicColor = musicEnabled ? SDL_Color{0, 255, 0, 255} : SDL_Color{255, 0, 0, 255};
|
||||
font->draw(renderer, popupX + 120, popupY + 70, musicStatus, 1.5f, musicColor);
|
||||
|
||||
// Sound effects toggle
|
||||
font->draw(renderer, popupX + 20, popupY + 100, "SOUND FX:", 1.5f, {255, 255, 255, 255});
|
||||
const char* soundStatus = soundEnabled ? "ON" : "OFF";
|
||||
SDL_Color soundColor = soundEnabled ? SDL_Color{0, 255, 0, 255} : SDL_Color{255, 0, 0, 255};
|
||||
font->draw(renderer, popupX + 140, popupY + 100, soundStatus, 1.5f, soundColor);
|
||||
|
||||
// Instructions
|
||||
font->draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, {200, 200, 220, 255});
|
||||
font->draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, {200, 200, 220, 255});
|
||||
font->draw(renderer, popupX + 20, popupY + 190, "ESC = CLOSE", 1.0f, {200, 200, 220, 255});
|
||||
}
|
||||
28
src/graphics/renderers/UIRenderer.h
Normal file
@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string>
|
||||
|
||||
class FontAtlas;
|
||||
|
||||
class UIRenderer {
|
||||
public:
|
||||
// Draw a sci-fi style panel with glow, shadow, and border
|
||||
static void drawSciFiPanel(SDL_Renderer* renderer, const SDL_FRect& rect, float alpha = 1.0f);
|
||||
|
||||
// Draw a generic button with hover/select states
|
||||
static void drawButton(SDL_Renderer* renderer, FontAtlas* font, float cx, float cy, float w, float h,
|
||||
const std::string& label, bool isHovered, bool isSelected,
|
||||
SDL_Color bgColor = {80, 110, 200, 255},
|
||||
SDL_Color borderColor = {60, 80, 140, 255},
|
||||
bool textOnly = false,
|
||||
SDL_Texture* icon = nullptr);
|
||||
|
||||
// Helper to calculate content offsets for centering
|
||||
static void computeContentOffsets(float winW, float winH, float logicalW, float logicalH, float logicalScale, float& outOffsetX, float& outOffsetY);
|
||||
|
||||
// Draw the game logo centered at the top
|
||||
static void drawLogo(SDL_Renderer* renderer, SDL_Texture* logoTex, float logicalW, float logicalH, float contentOffsetX, float contentOffsetY, int texW = 0, int texH = 0);
|
||||
|
||||
// Draw the settings popup
|
||||
static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas* font, float logicalW, float logicalH, bool musicEnabled, bool soundEnabled);
|
||||
};
|
||||
401
src/main.cpp
@ -25,6 +25,7 @@
|
||||
#include "persistence/Scores.h"
|
||||
#include "graphics/effects/Starfield.h"
|
||||
#include "graphics/effects/Starfield3D.h"
|
||||
#include "graphics/effects/SpaceWarp.h"
|
||||
#include "graphics/ui/Font.h"
|
||||
#include "graphics/ui/HelpOverlay.h"
|
||||
#include "gameplay/effects/LineEffect.h"
|
||||
@ -159,7 +160,7 @@ static bool queueLevelBackground(LevelBackgroundFader& fader, SDL_Renderer* rend
|
||||
}
|
||||
|
||||
char bgPath[256];
|
||||
std::snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.jpg", level);
|
||||
std::snprintf(bgPath, sizeof(bgPath), "assets/images/levels/level%d.jpg", level);
|
||||
|
||||
SDL_Texture* newTexture = loadTextureFromImage(renderer, bgPath);
|
||||
if (!newTexture) {
|
||||
@ -246,6 +247,31 @@ static void renderScaledBackground(SDL_Renderer* renderer, SDL_Texture* tex, int
|
||||
SDL_SetTextureAlphaMod(tex, 255);
|
||||
}
|
||||
|
||||
static void renderDynamicBackground(SDL_Renderer* renderer, SDL_Texture* tex, int winW, int winH, float baseScale, float motionClockMs, float alphaMul = 1.0f) {
|
||||
if (!renderer || !tex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float seconds = motionClockMs * 0.001f;
|
||||
const float wobble = std::max(0.4f, baseScale + std::sin(seconds * 0.07f) * 0.02f + std::sin(seconds * 0.23f) * 0.01f);
|
||||
const float rotation = std::sin(seconds * 0.035f) * 1.25f;
|
||||
const float panX = std::sin(seconds * 0.11f) * winW * 0.02f;
|
||||
const float panY = std::cos(seconds * 0.09f) * winH * 0.015f;
|
||||
|
||||
SDL_FRect dest{
|
||||
(winW - winW * wobble) * 0.5f + panX,
|
||||
(winH - winH * wobble) * 0.5f + panY,
|
||||
winW * wobble,
|
||||
winH * wobble
|
||||
};
|
||||
SDL_FPoint center{dest.w * 0.5f, dest.h * 0.5f};
|
||||
|
||||
Uint8 alpha = static_cast<Uint8>(std::clamp(alphaMul, 0.0f, 1.0f) * 255.0f);
|
||||
SDL_SetTextureAlphaMod(tex, alpha);
|
||||
SDL_RenderTextureRotated(renderer, tex, nullptr, &dest, rotation, ¢er, SDL_FLIP_NONE);
|
||||
SDL_SetTextureAlphaMod(tex, 255);
|
||||
}
|
||||
|
||||
static void drawOverlay(SDL_Renderer* renderer, const SDL_FRect& rect, SDL_Color color, Uint8 alpha) {
|
||||
if (!renderer || alpha == 0) {
|
||||
return;
|
||||
@ -256,7 +282,7 @@ static void drawOverlay(SDL_Renderer* renderer, const SDL_FRect& rect, SDL_Color
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
||||
}
|
||||
|
||||
static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Renderer* renderer, int winW, int winH) {
|
||||
static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Renderer* renderer, int winW, int winH, float motionClockMs) {
|
||||
if (!renderer) {
|
||||
return;
|
||||
}
|
||||
@ -264,12 +290,13 @@ static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Render
|
||||
SDL_FRect fullRect{0.f, 0.f, static_cast<float>(winW), static_cast<float>(winH)};
|
||||
const float duration = std::max(1.0f, fader.phaseDurationMs);
|
||||
const float progress = (fader.phase == LevelBackgroundPhase::Idle) ? 0.0f : std::clamp(fader.phaseElapsedMs / duration, 0.0f, 1.0f);
|
||||
const float seconds = motionClockMs * 0.001f;
|
||||
|
||||
switch (fader.phase) {
|
||||
case LevelBackgroundPhase::ZoomOut: {
|
||||
const float scale = 1.0f + progress * 0.15f;
|
||||
if (fader.currentTex) {
|
||||
renderScaledBackground(renderer, fader.currentTex, winW, winH, scale, Uint8((1.0f - progress * 0.4f) * 255.0f));
|
||||
renderDynamicBackground(renderer, fader.currentTex, winW, winH, scale, motionClockMs, (1.0f - progress * 0.4f));
|
||||
drawOverlay(renderer, fullRect, SDL_Color{0, 0, 0, 255}, Uint8(progress * 200.0f));
|
||||
}
|
||||
break;
|
||||
@ -278,16 +305,18 @@ static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Render
|
||||
const float scale = 1.10f - progress * 0.10f;
|
||||
const Uint8 alpha = Uint8((0.4f + progress * 0.6f) * 255.0f);
|
||||
if (fader.currentTex) {
|
||||
renderScaledBackground(renderer, fader.currentTex, winW, winH, scale, alpha);
|
||||
renderDynamicBackground(renderer, fader.currentTex, winW, winH, scale, motionClockMs, alpha / 255.0f);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LevelBackgroundPhase::Idle:
|
||||
default:
|
||||
if (fader.currentTex) {
|
||||
renderScaledBackground(renderer, fader.currentTex, winW, winH, 1.0f, 255);
|
||||
renderDynamicBackground(renderer, fader.currentTex, winW, winH, 1.02f, motionClockMs, 1.0f);
|
||||
float pulse = 0.35f + 0.25f * (0.5f + 0.5f * std::sin(seconds * 0.5f));
|
||||
drawOverlay(renderer, fullRect, SDL_Color{5, 12, 28, 255}, Uint8(pulse * 90.0f));
|
||||
} else if (fader.nextTex) {
|
||||
renderScaledBackground(renderer, fader.nextTex, winW, winH, 1.0f, 255);
|
||||
renderDynamicBackground(renderer, fader.nextTex, winW, winH, 1.02f, motionClockMs, 1.0f);
|
||||
} else {
|
||||
drawOverlay(renderer, fullRect, SDL_Color{0, 0, 0, 255}, 255);
|
||||
}
|
||||
@ -310,189 +339,7 @@ static void resetLevelBackgrounds(LevelBackgroundFader& fader) {
|
||||
|
||||
// ...existing code...
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Enhanced Button Drawing
|
||||
// -----------------------------------------------------------------------------
|
||||
static void drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
|
||||
const std::string& label, bool isHovered, bool isSelected = false) {
|
||||
SDL_Color bgColor = isHovered ? SDL_Color{120, 150, 240, 255} : SDL_Color{80, 110, 200, 255};
|
||||
if (isSelected) bgColor = {160, 190, 255, 255};
|
||||
|
||||
float x = cx - w/2;
|
||||
float y = cy - h/2;
|
||||
|
||||
// Draw button background with border
|
||||
drawRect(renderer, x-2, y-2, w+4, h+4, {60, 80, 140, 255}); // Border
|
||||
drawRect(renderer, x, y, w, h, bgColor); // Background
|
||||
|
||||
// Draw button text centered
|
||||
float textScale = 1.5f;
|
||||
float textX = x + (w - label.length() * 12 * textScale) / 2;
|
||||
float textY = y + (h - 20 * textScale) / 2;
|
||||
font.draw(renderer, textX, textY, label, textScale, {255, 255, 255, 255});
|
||||
}
|
||||
|
||||
// External wrapper for enhanced button so other translation units can call it.
|
||||
void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
|
||||
const std::string& label, bool isHovered, bool isSelected) {
|
||||
drawEnhancedButton(renderer, font, cx, cy, w, h, label, isHovered, isSelected);
|
||||
}
|
||||
|
||||
// Popup wrappers
|
||||
// Forward declarations for popup functions defined later in this file
|
||||
static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);
|
||||
|
||||
void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) {
|
||||
drawSettingsPopup(renderer, font, musicEnabled);
|
||||
}
|
||||
|
||||
// Simple rounded menu button drawer used by MenuState (keeps visual parity with JS)
|
||||
void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
|
||||
const std::string& label, SDL_Color bgColor, SDL_Color borderColor) {
|
||||
float x = cx - w/2;
|
||||
float y = cy - h/2;
|
||||
drawRect(renderer, x-6, y-6, w+12, h+12, borderColor);
|
||||
drawRect(renderer, x-4, y-4, w+8, h+8, {255,255,255,255});
|
||||
drawRect(renderer, x, y, w, h, bgColor);
|
||||
|
||||
float textScale = 1.6f;
|
||||
float approxCharW = 12.0f * textScale;
|
||||
float textW = label.length() * approxCharW;
|
||||
float tx = x + (w - textW) / 2.0f;
|
||||
float ty = y + (h - 20.0f * textScale) / 2.0f;
|
||||
font.draw(renderer, tx+2, ty+2, label, textScale, {0,0,0,180});
|
||||
font.draw(renderer, tx, ty, label, textScale, {255,255,255,255});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Block Drawing Functions
|
||||
// -----------------------------------------------------------------------------
|
||||
static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) {
|
||||
if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) {
|
||||
// Debug: print why we're falling back
|
||||
if (!blocksTex) {
|
||||
static bool printed = false;
|
||||
if (!printed) {
|
||||
(void)0;
|
||||
printed = true;
|
||||
}
|
||||
}
|
||||
// Fallback to colored rectangle if texture isn't available
|
||||
SDL_Color color = (blockType >= 0 && blockType < PIECE_COUNT) ? COLORS[blockType + 1] : SDL_Color{128, 128, 128, 255};
|
||||
drawRect(renderer, x, y, size-1, size-1, color);
|
||||
return;
|
||||
}
|
||||
|
||||
// JavaScript uses: sx = type * spriteSize, sy = 0, with 2px padding
|
||||
// Each sprite is 90px wide in the horizontal sprite sheet
|
||||
const int SPRITE_SIZE = 90;
|
||||
float srcX = blockType * SPRITE_SIZE + 2; // Add 2px padding like JS
|
||||
float srcY = 2; // Add 2px padding from top like JS
|
||||
float srcW = SPRITE_SIZE - 4; // Subtract 4px total padding like JS
|
||||
float srcH = SPRITE_SIZE - 4; // Subtract 4px total padding like JS
|
||||
|
||||
SDL_FRect srcRect = {srcX, srcY, srcW, srcH};
|
||||
SDL_FRect dstRect = {x, y, size, size};
|
||||
SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect);
|
||||
}
|
||||
|
||||
static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false) {
|
||||
if (piece.type >= PIECE_COUNT) return;
|
||||
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(piece, cx, cy)) {
|
||||
float px = ox + (piece.x + cx) * tileSize;
|
||||
float py = oy + (piece.y + cy) * tileSize;
|
||||
|
||||
if (isGhost) {
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// Draw ghost piece as barely visible gray outline
|
||||
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 20); // Very faint gray
|
||||
SDL_FRect rect = {px + 2, py + 2, tileSize - 4, tileSize - 4};
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
|
||||
// Draw thin gray border
|
||||
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 30);
|
||||
SDL_FRect border = {px + 1, py + 1, tileSize - 2, tileSize - 2};
|
||||
SDL_RenderRect(renderer, &border);
|
||||
} else {
|
||||
drawBlockTexture(renderer, blocksTex, px, py, tileSize, piece.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize) {
|
||||
if (pieceType >= PIECE_COUNT) return;
|
||||
|
||||
// Use the first rotation (index 0) for preview
|
||||
Game::Piece previewPiece;
|
||||
previewPiece.type = pieceType;
|
||||
previewPiece.rot = 0;
|
||||
previewPiece.x = 0;
|
||||
previewPiece.y = 0;
|
||||
|
||||
// Center the piece in the preview area
|
||||
float offsetX = 0, offsetY = 0;
|
||||
if (pieceType == I) { offsetX = tileSize * 0.5f; } // I-piece centering
|
||||
else if (pieceType == O) { offsetX = tileSize * 0.5f; } // O-piece centering
|
||||
|
||||
// Use semi-transparent alpha for preview blocks
|
||||
Uint8 previewAlpha = 180; // Change this value for more/less transparency
|
||||
SDL_SetTextureAlphaMod(blocksTex, previewAlpha);
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(previewPiece, cx, cy)) {
|
||||
float px = x + offsetX + cx * tileSize;
|
||||
float py = y + offsetY + cy * tileSize;
|
||||
drawBlockTexture(renderer, blocksTex, px, py, tileSize, pieceType);
|
||||
}
|
||||
}
|
||||
}
|
||||
SDL_SetTextureAlphaMod(blocksTex, 255); // Reset alpha after drawing
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Popup Drawing Functions
|
||||
// -----------------------------------------------------------------------------
|
||||
static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) {
|
||||
float popupW = 350, popupH = 260;
|
||||
float popupX = (LOGICAL_W - popupW) / 2;
|
||||
float popupY = (LOGICAL_H - popupH) / 2;
|
||||
|
||||
// Semi-transparent overlay
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 128);
|
||||
SDL_FRect overlay{0, 0, LOGICAL_W, LOGICAL_H};
|
||||
SDL_RenderFillRect(renderer, &overlay);
|
||||
|
||||
// Popup background
|
||||
drawRect(renderer, popupX-4, popupY-4, popupW+8, popupH+8, {100, 120, 160, 255}); // Border
|
||||
drawRect(renderer, popupX, popupY, popupW, popupH, {40, 50, 70, 255}); // Background
|
||||
|
||||
// Title
|
||||
font.draw(renderer, popupX + 20, popupY + 20, "SETTINGS", 2.0f, {255, 220, 0, 255});
|
||||
|
||||
// Music toggle
|
||||
font.draw(renderer, popupX + 20, popupY + 70, "MUSIC:", 1.5f, {255, 255, 255, 255});
|
||||
const char* musicStatus = musicEnabled ? "ON" : "OFF";
|
||||
SDL_Color musicColor = musicEnabled ? SDL_Color{0, 255, 0, 255} : SDL_Color{255, 0, 0, 255};
|
||||
font.draw(renderer, popupX + 120, popupY + 70, musicStatus, 1.5f, musicColor);
|
||||
|
||||
// Sound effects toggle
|
||||
font.draw(renderer, popupX + 20, popupY + 100, "SOUND FX:", 1.5f, {255, 255, 255, 255});
|
||||
const char* soundStatus = SoundEffectManager::instance().isEnabled() ? "ON" : "OFF";
|
||||
SDL_Color soundColor = SoundEffectManager::instance().isEnabled() ? SDL_Color{0, 255, 0, 255} : SDL_Color{255, 0, 0, 255};
|
||||
font.draw(renderer, popupX + 140, popupY + 100, soundStatus, 1.5f, soundColor);
|
||||
|
||||
// Instructions
|
||||
font.draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, {200, 200, 220, 255});
|
||||
font.draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, {200, 200, 220, 255});
|
||||
font.draw(renderer, popupX + 20, popupY + 190, "ESC = CLOSE", 1.0f, {200, 200, 220, 255});
|
||||
}
|
||||
// Legacy rendering functions removed (moved to UIRenderer / GameRenderer)
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@ -520,7 +367,9 @@ static bool helpOverlayPausedGame = false;
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tetris Block Fireworks for intro animation (block particles)
|
||||
// Forward declare block render helper used by particles
|
||||
static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType);
|
||||
// Forward declare block render helper used by particles
|
||||
// (Note: drawBlockTexture implementation was removed, so this is likely dead code unless particles use it.
|
||||
// However, particles use drawFireworks_impl which uses SDL_RenderGeometry, so this is unused.)
|
||||
// -----------------------------------------------------------------------------
|
||||
struct BlockParticle {
|
||||
float x{}, y{};
|
||||
@ -680,16 +529,15 @@ static void drawFireworks_impl(SDL_Renderer* renderer, SDL_Texture*) {
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, previousBlend);
|
||||
}
|
||||
// External wrappers for use by other translation units (MenuState)
|
||||
// Expect callers to pass the blocks texture via StateContext so we avoid globals.
|
||||
void menu_drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex) { drawFireworks_impl(renderer, blocksTex); }
|
||||
void menu_updateFireworks(double frameMs) { updateFireworks(frameMs); }
|
||||
// External wrappers retained for compatibility; now no-ops to disable the legacy fireworks effect.
|
||||
void menu_drawFireworks(SDL_Renderer*, SDL_Texture*) {}
|
||||
void menu_updateFireworks(double) {}
|
||||
double menu_getLogoAnimCounter() { return logoAnimCounter; }
|
||||
int menu_getHoveredButton() { return hoveredButton; }
|
||||
|
||||
int main(int, char **)
|
||||
{
|
||||
// Initialize random seed for fireworks
|
||||
// Initialize random seed for procedural effects
|
||||
srand(static_cast<unsigned int>(SDL_GetTicks()));
|
||||
|
||||
// Load settings
|
||||
@ -760,12 +608,13 @@ int main(int, char **)
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
FontAtlas font;
|
||||
font.init("FreeSans.ttf", 24);
|
||||
|
||||
// Load PressStart2P font for loading screen and retro UI elements
|
||||
// Primary UI font (Orbitron) used for major UI text: buttons, loading, HUD
|
||||
FontAtlas pixelFont;
|
||||
pixelFont.init("assets/fonts/PressStart2P-Regular.ttf", 16);
|
||||
pixelFont.init("assets/fonts/Orbitron.ttf", 22);
|
||||
|
||||
// Secondary font (Exo2) used for longer descriptions, settings, credits
|
||||
FontAtlas font;
|
||||
font.init("assets/fonts/Exo2.ttf", 20);
|
||||
|
||||
ScoreManager scores;
|
||||
std::atomic<bool> scoresLoadComplete{false};
|
||||
@ -779,21 +628,48 @@ int main(int, char **)
|
||||
starfield.init(200, LOGICAL_W, LOGICAL_H);
|
||||
Starfield3D starfield3D;
|
||||
starfield3D.init(LOGICAL_W, LOGICAL_H, 200);
|
||||
SpaceWarp spaceWarp;
|
||||
spaceWarp.init(LOGICAL_W, LOGICAL_H, 420);
|
||||
SpaceWarpFlightMode warpFlightMode = SpaceWarpFlightMode::Forward;
|
||||
spaceWarp.setFlightMode(warpFlightMode);
|
||||
bool warpAutoPilotEnabled = true;
|
||||
spaceWarp.setAutoPilotEnabled(true);
|
||||
|
||||
// Initialize line clearing effects
|
||||
LineEffect lineEffect;
|
||||
lineEffect.init(renderer);
|
||||
|
||||
// Load logo assets via SDL_image so we can use compressed formats
|
||||
SDL_Texture* logoTex = loadTextureFromImage(renderer, "assets/images/logo.bmp");
|
||||
SDL_Texture* logoTex = loadTextureFromImage(renderer, "assets/images/spacetris.png");
|
||||
|
||||
// Load small logo (used by Menu to show whole logo)
|
||||
int logoSmallW = 0, logoSmallH = 0;
|
||||
SDL_Texture* logoSmallTex = loadTextureFromImage(renderer, "assets/images/logo_small.bmp", &logoSmallW, &logoSmallH);
|
||||
SDL_Texture* logoSmallTex = loadTextureFromImage(renderer, "assets/images/spacetris.png", &logoSmallW, &logoSmallH);
|
||||
|
||||
// Load menu background using SDL_image (prefers JPEG)
|
||||
SDL_Texture* backgroundTex = loadTextureFromImage(renderer, "assets/images/main_background.bmp");
|
||||
|
||||
// Load the new main screen overlay that sits above the background but below buttons
|
||||
int mainScreenW = 0;
|
||||
int mainScreenH = 0;
|
||||
SDL_Texture* mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen.png", &mainScreenW, &mainScreenH);
|
||||
if (mainScreenTex) {
|
||||
SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded main_screen overlay %dx%d (tex=%p)", mainScreenW, mainScreenH, (void*)mainScreenTex);
|
||||
FILE* f = fopen("tetris_trace.log", "a");
|
||||
if (f) {
|
||||
fprintf(f, "main.cpp: loaded main_screen.bmp %dx%d tex=%p\n", mainScreenW, mainScreenH, (void*)mainScreenTex);
|
||||
fclose(f);
|
||||
}
|
||||
} else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to load assets/images/main_screen.bmp (overlay will be skipped)");
|
||||
FILE* f = fopen("tetris_trace.log", "a");
|
||||
if (f) {
|
||||
fprintf(f, "main.cpp: failed to load main_screen.bmp\n");
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: `backgroundTex` is owned by main and passed into `StateContext::backgroundTex` below.
|
||||
// States should render using `ctx.backgroundTex` rather than accessing globals.
|
||||
|
||||
@ -826,6 +702,19 @@ int main(int, char **)
|
||||
SDL_SetRenderTarget(renderer, nullptr);
|
||||
}
|
||||
|
||||
SDL_Texture* scorePanelTex = loadTextureFromImage(renderer, "assets/images/panel_score.png");
|
||||
if (scorePanelTex) {
|
||||
SDL_SetTextureBlendMode(scorePanelTex, SDL_BLENDMODE_BLEND);
|
||||
}
|
||||
SDL_Texture* statisticsPanelTex = loadTextureFromImage(renderer, "assets/images/statistics_panel.png");
|
||||
if (statisticsPanelTex) {
|
||||
SDL_SetTextureBlendMode(statisticsPanelTex, SDL_BLENDMODE_BLEND);
|
||||
}
|
||||
SDL_Texture* nextPanelTex = loadTextureFromImage(renderer, "assets/images/next_panel.png");
|
||||
if (nextPanelTex) {
|
||||
SDL_SetTextureBlendMode(nextPanelTex, SDL_BLENDMODE_BLEND);
|
||||
}
|
||||
|
||||
Game game(startLevelSelection);
|
||||
// Apply global gravity speed multiplier from config
|
||||
game.setGravityGlobalMultiplier(Config::Gameplay::GRAVITY_SPEED_MULTIPLIER);
|
||||
@ -950,6 +839,7 @@ int main(int, char **)
|
||||
int gameplayCountdownIndex = 0;
|
||||
const double GAMEPLAY_COUNTDOWN_STEP_MS = 400.0;
|
||||
const std::array<const char*, 4> GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" };
|
||||
double gameplayBackgroundClockMs = 0.0;
|
||||
|
||||
// Instantiate state manager
|
||||
StateManager stateMgr(state);
|
||||
@ -971,6 +861,12 @@ int main(int, char **)
|
||||
ctx.logoSmallH = logoSmallH;
|
||||
ctx.backgroundTex = backgroundTex;
|
||||
ctx.blocksTex = blocksTex;
|
||||
ctx.scorePanelTex = scorePanelTex;
|
||||
ctx.statisticsPanelTex = statisticsPanelTex;
|
||||
ctx.nextPanelTex = nextPanelTex;
|
||||
ctx.mainScreenTex = mainScreenTex;
|
||||
ctx.mainScreenW = mainScreenW;
|
||||
ctx.mainScreenH = mainScreenH;
|
||||
ctx.musicEnabled = &musicEnabled;
|
||||
ctx.startLevelSelection = &startLevelSelection;
|
||||
ctx.hoveredButton = &hoveredButton;
|
||||
@ -1168,6 +1064,40 @@ int main(int, char **)
|
||||
SDL_SetWindowFullscreen(window, isFullscreen ? SDL_WINDOW_FULLSCREEN : 0);
|
||||
Settings::instance().setFullscreen(isFullscreen);
|
||||
}
|
||||
if (e.key.scancode == SDL_SCANCODE_F5)
|
||||
{
|
||||
warpAutoPilotEnabled = false;
|
||||
warpFlightMode = SpaceWarpFlightMode::Forward;
|
||||
spaceWarp.setFlightMode(warpFlightMode);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Space warp mode: forward");
|
||||
}
|
||||
if (e.key.scancode == SDL_SCANCODE_F6)
|
||||
{
|
||||
warpAutoPilotEnabled = false;
|
||||
warpFlightMode = SpaceWarpFlightMode::BankLeft;
|
||||
spaceWarp.setFlightMode(warpFlightMode);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Space warp mode: bank left");
|
||||
}
|
||||
if (e.key.scancode == SDL_SCANCODE_F7)
|
||||
{
|
||||
warpAutoPilotEnabled = false;
|
||||
warpFlightMode = SpaceWarpFlightMode::BankRight;
|
||||
spaceWarp.setFlightMode(warpFlightMode);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Space warp mode: bank right");
|
||||
}
|
||||
if (e.key.scancode == SDL_SCANCODE_F8)
|
||||
{
|
||||
warpAutoPilotEnabled = false;
|
||||
warpFlightMode = SpaceWarpFlightMode::Reverse;
|
||||
spaceWarp.setFlightMode(warpFlightMode);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Space warp mode: reverse");
|
||||
}
|
||||
if (e.key.scancode == SDL_SCANCODE_F9)
|
||||
{
|
||||
warpAutoPilotEnabled = true;
|
||||
spaceWarp.setAutoPilotEnabled(true);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Space warp autopilot engaged");
|
||||
}
|
||||
}
|
||||
|
||||
// Text input for high score
|
||||
@ -1377,6 +1307,7 @@ int main(int, char **)
|
||||
|
||||
// Cap frame time to avoid spiral of death (max 100ms)
|
||||
if (frameMs > 100.0) frameMs = 100.0;
|
||||
gameplayBackgroundClockMs += frameMs;
|
||||
const bool *ks = SDL_GetKeyboardState(nullptr);
|
||||
bool left = state == AppState::Playing && ks[SDL_SCANCODE_LEFT];
|
||||
bool right = state == AppState::Playing && ks[SDL_SCANCODE_RIGHT];
|
||||
@ -1565,21 +1496,25 @@ int main(int, char **)
|
||||
}
|
||||
previousState = state;
|
||||
|
||||
// Update starfields based on current state
|
||||
// Update background effects
|
||||
if (state == AppState::Loading) {
|
||||
starfield3D.update(float(frameMs / 1000.0f));
|
||||
starfield3D.resize(logicalVP.w, logicalVP.h); // Update for window resize
|
||||
starfield3D.resize(winW, winH);
|
||||
} else {
|
||||
starfield.update(float(frameMs / 1000.0f), logicalVP.x * 2 + logicalVP.w, logicalVP.y * 2 + logicalVP.h);
|
||||
}
|
||||
|
||||
if (state == AppState::Menu) {
|
||||
spaceWarp.resize(winW, winH);
|
||||
spaceWarp.update(float(frameMs / 1000.0f));
|
||||
}
|
||||
|
||||
// Advance level background fade if a next texture is queued
|
||||
updateLevelBackgroundFade(levelBackgrounds, float(frameMs));
|
||||
|
||||
// Update intro animations
|
||||
if (state == AppState::Menu) {
|
||||
logoAnimCounter += frameMs * 0.0008; // Animation speed
|
||||
updateFireworks(frameMs);
|
||||
}
|
||||
|
||||
// --- Per-state update hooks (allow states to manage logic incrementally)
|
||||
@ -1671,19 +1606,23 @@ int main(int, char **)
|
||||
|
||||
// --- Render ---
|
||||
SDL_SetRenderViewport(renderer, nullptr);
|
||||
SDL_SetRenderDrawColor(renderer, 12, 12, 16, 255);
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
||||
SDL_RenderClear(renderer);
|
||||
|
||||
// Draw level-based background for gameplay, starfield for other states
|
||||
if (state == AppState::Playing) {
|
||||
int bgLevel = std::clamp(game.level(), 0, 32);
|
||||
queueLevelBackground(levelBackgrounds, renderer, bgLevel);
|
||||
renderLevelBackgrounds(levelBackgrounds, renderer, winW, winH);
|
||||
renderLevelBackgrounds(levelBackgrounds, renderer, winW, winH, static_cast<float>(gameplayBackgroundClockMs));
|
||||
} else if (state == AppState::Loading) {
|
||||
// Use 3D starfield for loading screen (full screen)
|
||||
starfield3D.draw(renderer);
|
||||
} else if (state == AppState::Menu || state == AppState::LevelSelector || state == AppState::Options) {
|
||||
// Use static background for menu, stretched to window; no starfield on sides
|
||||
} else if (state == AppState::Menu) {
|
||||
// Space flyover backdrop for the main screen
|
||||
spaceWarp.draw(renderer, 1.0f);
|
||||
// `mainScreenTex` is rendered as a top layer just before presenting
|
||||
// so we don't draw it here. Keep the space warp background only.
|
||||
} else if (state == AppState::LevelSelector || state == AppState::Options) {
|
||||
if (backgroundTex) {
|
||||
SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH };
|
||||
SDL_RenderTexture(renderer, backgroundTex, nullptr, &fullRect);
|
||||
@ -1824,6 +1763,9 @@ int main(int, char **)
|
||||
&pixelFont,
|
||||
&lineEffect,
|
||||
blocksTex,
|
||||
ctx.statisticsPanelTex,
|
||||
scorePanelTex,
|
||||
nextPanelTex,
|
||||
(float)LOGICAL_W,
|
||||
(float)LOGICAL_H,
|
||||
logicalScale,
|
||||
@ -2014,6 +1956,43 @@ int main(int, char **)
|
||||
HelpOverlay::Render(renderer, pixelFont, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY);
|
||||
}
|
||||
|
||||
// Top-layer overlay: render `mainScreenTex` above all other layers when in Menu
|
||||
if (state == AppState::Menu && mainScreenTex) {
|
||||
SDL_SetRenderViewport(renderer, nullptr);
|
||||
SDL_SetRenderScale(renderer, 1.f, 1.f);
|
||||
float texW = mainScreenW > 0 ? static_cast<float>(mainScreenW) : 0.0f;
|
||||
float texH = mainScreenH > 0 ? static_cast<float>(mainScreenH) : 0.0f;
|
||||
if (texW <= 0.0f || texH <= 0.0f) {
|
||||
float iwf = 0.0f, ihf = 0.0f;
|
||||
if (SDL_GetTextureSize(mainScreenTex, &iwf, &ihf) != 0) {
|
||||
iwf = ihf = 0.0f;
|
||||
}
|
||||
texW = iwf;
|
||||
texH = ihf;
|
||||
}
|
||||
if (texW > 0.0f && texH > 0.0f) {
|
||||
const float drawH = static_cast<float>(winH);
|
||||
const float scale = drawH / texH;
|
||||
const float drawW = texW * scale;
|
||||
SDL_FRect dst{
|
||||
(winW - drawW) * 0.5f,
|
||||
0.0f,
|
||||
drawW,
|
||||
drawH
|
||||
};
|
||||
SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND);
|
||||
SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst);
|
||||
}
|
||||
// Restore logical viewport/scale and draw the main PLAY button above the overlay
|
||||
SDL_SetRenderViewport(renderer, &logicalVP);
|
||||
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
|
||||
if (menuState) {
|
||||
menuState->drawMainButtonNormally = false; // ensure it isn't double-drawn
|
||||
menuState->renderMainButtonTop(renderer, logicalScale, logicalVP);
|
||||
menuState->drawMainButtonNormally = true;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_RenderPresent(renderer);
|
||||
SDL_SetRenderScale(renderer, 1.f, 1.f);
|
||||
}
|
||||
@ -2021,9 +2000,13 @@ int main(int, char **)
|
||||
SDL_DestroyTexture(logoTex);
|
||||
if (backgroundTex)
|
||||
SDL_DestroyTexture(backgroundTex);
|
||||
if (mainScreenTex)
|
||||
SDL_DestroyTexture(mainScreenTex);
|
||||
resetLevelBackgrounds(levelBackgrounds);
|
||||
if (blocksTex)
|
||||
SDL_DestroyTexture(blocksTex);
|
||||
if (scorePanelTex)
|
||||
SDL_DestroyTexture(scorePanelTex);
|
||||
if (logoSmallTex)
|
||||
SDL_DestroyTexture(logoSmallTex);
|
||||
|
||||
|
||||