latest state
This commit is contained in:
@ -48,6 +48,7 @@ set(TETRIS_SOURCES
|
|||||||
src/graphics/ui/Font.cpp
|
src/graphics/ui/Font.cpp
|
||||||
src/graphics/ui/HelpOverlay.cpp
|
src/graphics/ui/HelpOverlay.cpp
|
||||||
src/graphics/renderers/GameRenderer.cpp
|
src/graphics/renderers/GameRenderer.cpp
|
||||||
|
src/graphics/renderers/UIRenderer.cpp
|
||||||
src/audio/Audio.cpp
|
src/audio/Audio.cpp
|
||||||
src/gameplay/effects/LineEffect.cpp
|
src/gameplay/effects/LineEffect.cpp
|
||||||
src/audio/SoundEffect.cpp
|
src/audio/SoundEffect.cpp
|
||||||
|
|||||||
760
CODE_ANALYSIS.md
Normal file
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
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
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/images/earth_back.jpg
Normal file
BIN
assets/images/earth_back.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 513 KiB |
BIN
assets/images/spacetris.png
Normal file
BIN
assets/images/spacetris.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 486 KiB |
@ -6,13 +6,13 @@ Fullscreen=1
|
|||||||
|
|
||||||
[Audio]
|
[Audio]
|
||||||
Music=1
|
Music=1
|
||||||
Sound=1
|
Sound=0
|
||||||
|
|
||||||
[Gameplay]
|
[Gameplay]
|
||||||
SmoothScroll=1
|
SmoothScroll=1
|
||||||
|
|
||||||
[Player]
|
[Player]
|
||||||
Name=PLAYER
|
Name=GREGOR
|
||||||
|
|
||||||
[Debug]
|
[Debug]
|
||||||
Enabled=1
|
Enabled=1
|
||||||
|
|||||||
@ -12,10 +12,7 @@ void menu_updateFireworks(double frameMs);
|
|||||||
double menu_getLogoAnimCounter();
|
double menu_getLogoAnimCounter();
|
||||||
int menu_getHoveredButton();
|
int menu_getHoveredButton();
|
||||||
|
|
||||||
void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
|
// Legacy wrappers removed
|
||||||
const std::string& label, bool isHovered, bool isSelected);
|
// void menu_drawEnhancedButton(...);
|
||||||
|
// void menu_drawMenuButton(...);
|
||||||
void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
|
// void menu_drawSettingsPopup(...);
|
||||||
const std::string& label, SDL_Color bgColor, SDL_Color borderColor);
|
|
||||||
|
|
||||||
void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ namespace Config {
|
|||||||
constexpr int MAX_LEVELS = 20; // Maximum selectable starting level
|
constexpr int MAX_LEVELS = 20; // Maximum selectable starting level
|
||||||
|
|
||||||
// Gravity speed multiplier: 1.0 = normal, 2.0 = 2x slower, 0.5 = 2x faster
|
// 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
|
// UI Layout constants
|
||||||
|
|||||||
@ -20,6 +20,8 @@ void SpaceWarp::init(int w, int h, int starCount) {
|
|||||||
for (auto& star : stars) {
|
for (auto& star : stars) {
|
||||||
respawn(star, true);
|
respawn(star, true);
|
||||||
}
|
}
|
||||||
|
comets.clear();
|
||||||
|
cometSpawnTimer = randomRange(settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpaceWarp::resize(int w, int h) {
|
void SpaceWarp::resize(int w, int h) {
|
||||||
@ -33,6 +35,7 @@ void SpaceWarp::resize(int w, int h) {
|
|||||||
void SpaceWarp::setSettings(const SpaceWarpSettings& newSettings) {
|
void SpaceWarp::setSettings(const SpaceWarpSettings& newSettings) {
|
||||||
settings = newSettings;
|
settings = newSettings;
|
||||||
warpFactor = std::max(width, height) * settings.warpFactorScale;
|
warpFactor = std::max(width, height) * settings.warpFactorScale;
|
||||||
|
cometSpawnTimer = std::clamp(cometSpawnTimer, settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpaceWarp::setFlightMode(SpaceWarpFlightMode mode) {
|
void SpaceWarp::setFlightMode(SpaceWarpFlightMode mode) {
|
||||||
@ -69,6 +72,7 @@ void SpaceWarp::setAutoPilotEnabled(bool enabled) {
|
|||||||
flightMode = SpaceWarpFlightMode::Custom;
|
flightMode = SpaceWarpFlightMode::Custom;
|
||||||
motionTarget = motion;
|
motionTarget = motion;
|
||||||
autoTimer = 0.0f;
|
autoTimer = 0.0f;
|
||||||
|
scheduleNewAutoTarget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +86,32 @@ void SpaceWarp::scheduleNewAutoTarget() {
|
|||||||
autoTimer = randomRange(autoMinInterval, autoMaxInterval);
|
autoTimer = randomRange(autoMinInterval, autoMaxInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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};
|
||||||
|
comet.prevScreenX = centerX;
|
||||||
|
comet.prevScreenY = centerY;
|
||||||
|
comet.screenX = centerX;
|
||||||
|
comet.screenY = centerY;
|
||||||
|
comets.push_back(comet);
|
||||||
|
}
|
||||||
|
|
||||||
float SpaceWarp::randomRange(float min, float max) {
|
float SpaceWarp::randomRange(float min, float max) {
|
||||||
std::uniform_real_distribution<float> dist(min, max);
|
std::uniform_real_distribution<float> dist(min, max);
|
||||||
return dist(rng);
|
return dist(rng);
|
||||||
@ -112,12 +142,16 @@ void SpaceWarp::respawn(WarpStar& star, bool randomDepth) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool SpaceWarp::project(const WarpStar& star, float& outX, float& outY) const {
|
bool SpaceWarp::project(const WarpStar& star, float& outX, float& outY) const {
|
||||||
if (star.z <= minDepth) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
float perspective = warpFactor / (star.z + 0.001f);
|
float perspective = warpFactor / (z + 0.001f);
|
||||||
outX = centerX + star.x * perspective;
|
outX = centerX + x * perspective;
|
||||||
outY = centerY + star.y * perspective;
|
outY = centerY + y * perspective;
|
||||||
const float margin = settings.spawnMargin;
|
const float margin = settings.spawnMargin;
|
||||||
return outX >= -margin && outX <= width + margin && outY >= -margin && outY <= height + margin;
|
return outX >= -margin && outX <= width + margin && outY >= -margin && outY <= height + margin;
|
||||||
}
|
}
|
||||||
@ -127,6 +161,14 @@ void SpaceWarp::update(float deltaSeconds) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.cometSpawnIntervalMax > 0.0f) {
|
||||||
|
cometSpawnTimer -= deltaSeconds;
|
||||||
|
if (cometSpawnTimer <= 0.0f) {
|
||||||
|
spawnComet();
|
||||||
|
cometSpawnTimer = randomRange(settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (autoPilotEnabled) {
|
if (autoPilotEnabled) {
|
||||||
autoTimer -= deltaSeconds;
|
autoTimer -= deltaSeconds;
|
||||||
if (autoTimer <= 0.0f) {
|
if (autoTimer <= 0.0f) {
|
||||||
@ -188,6 +230,51 @@ void SpaceWarp::update(float deltaSeconds) {
|
|||||||
star.prevScreenY = star.screenY - dy * 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) {
|
void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) {
|
||||||
@ -224,5 +311,16 @@ void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) {
|
|||||||
SDL_RenderFillRect(renderer, &dot);
|
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);
|
SDL_SetRenderDrawBlendMode(renderer, previous);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,14 @@ struct SpaceWarpSettings {
|
|||||||
bool drawTrails = true; // when true, also render streak lines for hyper-speed look
|
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 trailAlphaScale = 0.75f; // relative opacity for streak lines vs dots
|
||||||
float maxTrailLength = 36.0f; // clamp length of each streak in pixels
|
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 {
|
struct SpaceWarpFlightMotion {
|
||||||
@ -69,11 +77,30 @@ private:
|
|||||||
Uint8 baseShade = 220;
|
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);
|
void respawn(WarpStar& star, bool randomDepth = true);
|
||||||
bool project(const WarpStar& star, float& outX, float& outY) const;
|
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);
|
float randomRange(float min, float max);
|
||||||
|
void spawnComet();
|
||||||
|
|
||||||
std::vector<WarpStar> stars;
|
std::vector<WarpStar> stars;
|
||||||
|
std::vector<WarpComet> comets;
|
||||||
std::mt19937 rng;
|
std::mt19937 rng;
|
||||||
|
|
||||||
int width = 0;
|
int width = 0;
|
||||||
@ -90,6 +117,7 @@ private:
|
|||||||
float autoMinInterval = 3.5f;
|
float autoMinInterval = 3.5f;
|
||||||
float autoMaxInterval = 7.5f;
|
float autoMaxInterval = 7.5f;
|
||||||
SpaceWarpFlightMotion motionTarget{};
|
SpaceWarpFlightMotion motionTarget{};
|
||||||
|
float cometSpawnTimer = 0.0f;
|
||||||
|
|
||||||
float minDepth = 2.0f;
|
float minDepth = 2.0f;
|
||||||
float maxDepth = 320.0f;
|
float maxDepth = 320.0f;
|
||||||
|
|||||||
188
src/graphics/renderers/UIRenderer.cpp
Normal file
188
src/graphics/renderers/UIRenderer.cpp
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
#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};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
float textScale = 1.5f;
|
||||||
|
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
|
||||||
|
float ty = y + (h - static_cast<float>(textH)) * 0.5f + 2.0f;
|
||||||
|
|
||||||
|
// 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
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);
|
||||||
|
};
|
||||||
192
src/main.cpp
192
src/main.cpp
@ -311,189 +311,7 @@ static void resetLevelBackgrounds(LevelBackgroundFader& fader) {
|
|||||||
|
|
||||||
// ...existing code...
|
// ...existing code...
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// Legacy rendering functions removed (moved to UIRenderer / GameRenderer)
|
||||||
// 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});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@ -521,7 +339,9 @@ static bool helpOverlayPausedGame = false;
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Tetris Block Fireworks for intro animation (block particles)
|
// Tetris Block Fireworks for intro animation (block particles)
|
||||||
// Forward declare block render helper used by 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 {
|
struct BlockParticle {
|
||||||
float x{}, y{};
|
float x{}, y{};
|
||||||
@ -791,11 +611,11 @@ int main(int, char **)
|
|||||||
lineEffect.init(renderer);
|
lineEffect.init(renderer);
|
||||||
|
|
||||||
// Load logo assets via SDL_image so we can use compressed formats
|
// 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)
|
// Load small logo (used by Menu to show whole logo)
|
||||||
int logoSmallW = 0, logoSmallH = 0;
|
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)
|
// Load menu background using SDL_image (prefers JPEG)
|
||||||
SDL_Texture* backgroundTex = loadTextureFromImage(renderer, "assets/images/main_background.bmp");
|
SDL_Texture* backgroundTex = loadTextureFromImage(renderer, "assets/images/main_background.bmp");
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include "../graphics/renderers/UIRenderer.h"
|
||||||
|
|
||||||
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
|
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
|
||||||
|
|
||||||
@ -93,30 +94,7 @@ static void Vignette(SDL_Renderer* r, int w, int h) {
|
|||||||
FillRect(r, SDL_FRect{(float)w - pad, 0, (float)pad, (float)h}, SDL_Color{0, 0, 0, 140});
|
FillRect(r, SDL_FRect{(float)w - pad, 0, (float)pad, (float)h}, SDL_Color{0, 0, 0, 140});
|
||||||
}
|
}
|
||||||
|
|
||||||
static SDL_FRect DrawPanel(SDL_Renderer* r, float w, float h, bool draw = true, float offX = 0.f, float offY = 0.f) {
|
// DrawPanel removed, replaced by UIRenderer::drawSciFiPanel
|
||||||
float PW = std::min(520.f, w * 0.65f);
|
|
||||||
float PH = std::min(360.f, h * 0.7f);
|
|
||||||
SDL_FRect p{ (w - PW) / 2.f + offX, (h - PH) / 2.f - 40.f + offY, PW, PH }; // Moved up by 50px
|
|
||||||
|
|
||||||
if (!draw) return p; // geometry only
|
|
||||||
|
|
||||||
// drop shadow
|
|
||||||
FillRect(r, SDL_FRect{p.x + 6, p.y + 10, p.w, p.h}, SDL_Color{0, 0, 0, 120});
|
|
||||||
// glow aura
|
|
||||||
for (int i = 0; i < 6; i++) {
|
|
||||||
SDL_FRect g{ p.x - (float)(i * 2), p.y - (float)(i * 2), p.w + (float)(i * 4), p.h + (float)(i * 4) };
|
|
||||||
SDL_Color c = COL_CYAN_SO; c.a = (Uint8)(36 - i * 6);
|
|
||||||
StrokeRect(r, g, c);
|
|
||||||
}
|
|
||||||
// outer body + border
|
|
||||||
FillRect(r, p, COL_PANEL);
|
|
||||||
StrokeRect(r, p, COL_CYAN);
|
|
||||||
|
|
||||||
// inner face
|
|
||||||
FillRect(r, SDL_FRect{p.x + 12, p.y + 56, p.w - 24, p.h - 68}, COL_PANEL_IN);
|
|
||||||
StrokeRect(r, SDL_FRect{p.x + 12, p.y + 56, p.w - 24, p.h - 68}, SDL_Color{24, 31, 41, 180});
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Grid {
|
struct Grid {
|
||||||
int cols = 4, rows = 5;
|
int cols = 4, rows = 5;
|
||||||
@ -195,7 +173,17 @@ void LevelSelectorState::handleEvent(const SDL_Event& e) {
|
|||||||
float ly = (float(e.button.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f);
|
float ly = (float(e.button.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f);
|
||||||
|
|
||||||
// Use same panel calculation as render (centered)
|
// Use same panel calculation as render (centered)
|
||||||
SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
|
const float LOGICAL_W_F = 1200.f;
|
||||||
|
const float LOGICAL_H_F = 1000.f;
|
||||||
|
float winW = (float)lastLogicalVP.w;
|
||||||
|
float winH = (float)lastLogicalVP.h;
|
||||||
|
float contentOffsetX = 0.0f;
|
||||||
|
float contentOffsetY = 0.0f;
|
||||||
|
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY);
|
||||||
|
|
||||||
|
float PW = std::min(520.f, LOGICAL_W_F * 0.65f);
|
||||||
|
float PH = std::min(360.f, LOGICAL_H_F * 0.7f);
|
||||||
|
SDL_FRect panel{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH };
|
||||||
Grid g = MakeGrid(panel);
|
Grid g = MakeGrid(panel);
|
||||||
int hit = HitTest(g, int(lx), int(ly));
|
int hit = HitTest(g, int(lx), int(ly));
|
||||||
if (hit != -1) {
|
if (hit != -1) {
|
||||||
@ -214,7 +202,17 @@ void LevelSelectorState::handleEvent(const SDL_Event& e) {
|
|||||||
float ly = (float(e.motion.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f);
|
float ly = (float(e.motion.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f);
|
||||||
|
|
||||||
// Use same panel calculation as render (centered)
|
// Use same panel calculation as render (centered)
|
||||||
SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
|
const float LOGICAL_W_F = 1200.f;
|
||||||
|
const float LOGICAL_H_F = 1000.f;
|
||||||
|
float winW = (float)lastLogicalVP.w;
|
||||||
|
float winH = (float)lastLogicalVP.h;
|
||||||
|
float contentOffsetX = 0.0f;
|
||||||
|
float contentOffsetY = 0.0f;
|
||||||
|
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY);
|
||||||
|
|
||||||
|
float PW = std::min(520.f, LOGICAL_W_F * 0.65f);
|
||||||
|
float PH = std::min(360.f, LOGICAL_H_F * 0.7f);
|
||||||
|
SDL_FRect panel{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH };
|
||||||
Grid g = MakeGrid(panel);
|
Grid g = MakeGrid(panel);
|
||||||
hoveredLevel = HitTest(g, int(lx), int(ly));
|
hoveredLevel = HitTest(g, int(lx), int(ly));
|
||||||
}
|
}
|
||||||
@ -242,29 +240,30 @@ void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer, float l
|
|||||||
// Compute content offsets (same approach as MenuState for proper centering)
|
// Compute content offsets (same approach as MenuState for proper centering)
|
||||||
float winW = (float)logicalVP.w;
|
float winW = (float)logicalVP.w;
|
||||||
float winH = (float)logicalVP.h;
|
float winH = (float)logicalVP.h;
|
||||||
float contentW = LOGICAL_W * logicalScale;
|
float contentOffsetX = 0.0f;
|
||||||
float contentH = LOGICAL_H * logicalScale;
|
float contentOffsetY = 0.0f;
|
||||||
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY);
|
||||||
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
|
||||||
|
|
||||||
// Draw the logo at the top (same as MenuState)
|
// Draw the logo at the top (same as MenuState)
|
||||||
SDL_Texture* logoToUse = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex;
|
SDL_Texture* logoToUse = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex;
|
||||||
if (logoToUse) {
|
int logoW = 0, logoH = 0;
|
||||||
// Use dimensions provided by the shared context when available
|
if (logoToUse == ctx.logoSmallTex && ctx.logoSmallW > 0) {
|
||||||
int texW = (logoToUse == ctx.logoSmallTex && ctx.logoSmallW > 0) ? ctx.logoSmallW : 872;
|
logoW = ctx.logoSmallW;
|
||||||
int texH = (logoToUse == ctx.logoSmallTex && ctx.logoSmallH > 0) ? ctx.logoSmallH : 273;
|
logoH = ctx.logoSmallH;
|
||||||
float maxW = LOGICAL_W * 0.6f; // Match MenuState and OptionsState
|
|
||||||
float scale = std::min(1.0f, maxW / float(texW));
|
|
||||||
float dw = texW * scale;
|
|
||||||
float dh = texH * scale;
|
|
||||||
float logoX = (LOGICAL_W - dw) / 2.f + contentOffsetX;
|
|
||||||
float logoY = LOGICAL_H * 0.05f + contentOffsetY; // Match MenuState and OptionsState
|
|
||||||
SDL_FRect dst{logoX, logoY, dw, dh};
|
|
||||||
SDL_RenderTexture(renderer, logoToUse, nullptr, &dst);
|
|
||||||
}
|
}
|
||||||
|
UIRenderer::drawLogo(renderer, logoToUse, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY, logoW, logoH);
|
||||||
|
|
||||||
// Panel and title strip (in logical space) - centered properly with offsets
|
// Panel and title strip (in logical space) - centered properly with offsets
|
||||||
SDL_FRect panel = DrawPanel(renderer, LOGICAL_W, LOGICAL_H, /*draw=*/true, contentOffsetX, contentOffsetY);
|
float PW = std::min(520.f, LOGICAL_W * 0.65f);
|
||||||
|
float PH = std::min(360.f, LOGICAL_H * 0.7f);
|
||||||
|
SDL_FRect panel{ (LOGICAL_W - PW) / 2.f + contentOffsetX, (LOGICAL_H - PH) / 2.f - 40.f + contentOffsetY, PW, PH };
|
||||||
|
|
||||||
|
UIRenderer::drawSciFiPanel(renderer, panel);
|
||||||
|
|
||||||
|
// Inner face (LevelSelector specific)
|
||||||
|
SDL_FRect inner{panel.x + 12, panel.y + 56, panel.w - 24, panel.h - 68};
|
||||||
|
FillRect(renderer, inner, COL_PANEL_IN);
|
||||||
|
StrokeRect(renderer, inner, SDL_Color{24, 31, 41, 180});
|
||||||
|
|
||||||
// Title text - prefer pixelFont for a blocky title if available, fallback to regular font
|
// Title text - prefer pixelFont for a blocky title if available, fallback to regular font
|
||||||
FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
|
FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
|
||||||
@ -296,7 +295,17 @@ bool LevelSelectorState::isMouseInPopup(float mouseX, float mouseY, float& popup
|
|||||||
lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale;
|
lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale;
|
||||||
ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale;
|
ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale;
|
||||||
}
|
}
|
||||||
SDL_FRect p = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
|
const float LOGICAL_W_F = 1200.f;
|
||||||
|
const float LOGICAL_H_F = 1000.f;
|
||||||
|
float winW = (float)lastLogicalVP.w;
|
||||||
|
float winH = (float)lastLogicalVP.h;
|
||||||
|
float contentOffsetX = 0.0f;
|
||||||
|
float contentOffsetY = 0.0f;
|
||||||
|
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY);
|
||||||
|
|
||||||
|
float PW = std::min(520.f, LOGICAL_W_F * 0.65f);
|
||||||
|
float PH = std::min(360.f, LOGICAL_H_F * 0.7f);
|
||||||
|
SDL_FRect p{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH };
|
||||||
popupX = p.x; popupY = p.y; popupW = p.w; popupH = p.h;
|
popupX = p.x; popupY = p.y; popupW = p.w; popupH = p.h;
|
||||||
return lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH;
|
return lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH;
|
||||||
}
|
}
|
||||||
@ -312,7 +321,17 @@ int LevelSelectorState::getLevelFromMouse(float mouseX, float mouseY, float popu
|
|||||||
lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale;
|
lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale;
|
||||||
ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale;
|
ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale;
|
||||||
}
|
}
|
||||||
SDL_FRect p = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
|
const float LOGICAL_W_F = 1200.f;
|
||||||
|
const float LOGICAL_H_F = 1000.f;
|
||||||
|
float winW = (float)lastLogicalVP.w;
|
||||||
|
float winH = (float)lastLogicalVP.h;
|
||||||
|
float contentOffsetX = 0.0f;
|
||||||
|
float contentOffsetY = 0.0f;
|
||||||
|
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY);
|
||||||
|
|
||||||
|
float PW = std::min(520.f, LOGICAL_W_F * 0.65f);
|
||||||
|
float PH = std::min(360.f, LOGICAL_H_F * 0.7f);
|
||||||
|
SDL_FRect p{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH };
|
||||||
Grid g = MakeGrid(p);
|
Grid g = MakeGrid(p);
|
||||||
return HitTest(g, (int)lx, (int)ly);
|
return HitTest(g, (int)lx, (int)ly);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include "../core/GlobalState.h"
|
#include "../core/GlobalState.h"
|
||||||
#include "../core/state/StateManager.h"
|
#include "../core/state/StateManager.h"
|
||||||
#include "../audio/Audio.h"
|
#include "../audio/Audio.h"
|
||||||
|
#include "../audio/SoundEffect.h"
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@ -20,6 +21,8 @@
|
|||||||
// Menu helper wrappers are declared in a shared header implemented in main.cpp
|
// Menu helper wrappers are declared in a shared header implemented in main.cpp
|
||||||
#include "../audio/MenuWrappers.h"
|
#include "../audio/MenuWrappers.h"
|
||||||
#include "../utils/ImagePathResolver.h"
|
#include "../utils/ImagePathResolver.h"
|
||||||
|
#include "../graphics/renderers/UIRenderer.h"
|
||||||
|
#include "../graphics/renderers/GameRenderer.h"
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
|
||||||
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
|
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
|
||||||
@ -38,6 +41,12 @@ void MenuState::onExit() {
|
|||||||
if (ctx.showExitConfirmPopup) {
|
if (ctx.showExitConfirmPopup) {
|
||||||
*ctx.showExitConfirmPopup = false;
|
*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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuState::handleEvent(const SDL_Event& e) {
|
void MenuState::handleEvent(const SDL_Event& e) {
|
||||||
@ -177,10 +186,9 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
|||||||
// Compute content offsets (same approach as main.cpp for proper centering)
|
// Compute content offsets (same approach as main.cpp for proper centering)
|
||||||
float winW = (float)logicalVP.w;
|
float winW = (float)logicalVP.w;
|
||||||
float winH = (float)logicalVP.h;
|
float winH = (float)logicalVP.h;
|
||||||
float contentW = LOGICAL_W * logicalScale;
|
float contentOffsetX = 0.0f;
|
||||||
float contentH = LOGICAL_H * logicalScale;
|
float contentOffsetY = 0.0f;
|
||||||
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY);
|
||||||
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
|
||||||
|
|
||||||
// Background is drawn by main (stretched to the full window) to avoid double-draw.
|
// Background is drawn by main (stretched to the full window) to avoid double-draw.
|
||||||
{
|
{
|
||||||
@ -250,106 +258,19 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the sci-fi overlay that sits above the scoreboard but below the buttons
|
// The main_screen overlay is drawn by main.cpp as the background
|
||||||
SDL_Texture* overlayTex = ctx.mainScreenTex;
|
// We don't need to draw it again here as a logo
|
||||||
int overlayW = ctx.mainScreenW;
|
|
||||||
int overlayH = ctx.mainScreenH;
|
|
||||||
|
|
||||||
static SDL_Texture* fallbackOverlay = nullptr;
|
|
||||||
static int fallbackW = 0;
|
|
||||||
static int fallbackH = 0;
|
|
||||||
|
|
||||||
if (!overlayTex) {
|
|
||||||
if (!fallbackOverlay) {
|
|
||||||
const std::string resolvedOverlay = AssetPath::resolveImagePath("assets/images/main_screen.bmp");
|
|
||||||
fallbackOverlay = IMG_LoadTexture(renderer, resolvedOverlay.c_str());
|
|
||||||
if (fallbackOverlay) {
|
|
||||||
SDL_SetTextureBlendMode(fallbackOverlay, SDL_BLENDMODE_BLEND);
|
|
||||||
float tmpW = 0.0f;
|
|
||||||
float tmpH = 0.0f;
|
|
||||||
SDL_GetTextureSize(fallbackOverlay, &tmpW, &tmpH);
|
|
||||||
fallbackW = static_cast<int>(tmpW);
|
|
||||||
fallbackH = static_cast<int>(tmpH);
|
|
||||||
FILE* f = fopen("tetris_trace.log", "a");
|
|
||||||
if (f) {
|
|
||||||
fprintf(f, "MenuState::render loaded fallback overlay texture %p path=%s size=%dx%d\n",
|
|
||||||
(void*)fallbackOverlay, resolvedOverlay.c_str(), fallbackW, fallbackH);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
FILE* f = fopen("tetris_trace.log", "a");
|
|
||||||
if (f) {
|
|
||||||
fprintf(f, "MenuState::render failed to load fallback overlay: %s\n", SDL_GetError());
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
overlayTex = fallbackOverlay;
|
|
||||||
overlayW = fallbackW;
|
|
||||||
overlayH = fallbackH;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlayTex) {
|
|
||||||
{
|
|
||||||
FILE* f = fopen("tetris_trace.log", "a");
|
|
||||||
if (f) {
|
|
||||||
fprintf(f, "MenuState::render overlay tex=%llu dims=%dx%d\n",
|
|
||||||
(unsigned long long)(uintptr_t)overlayTex,
|
|
||||||
overlayW,
|
|
||||||
overlayH);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
float texW = overlayW > 0 ? static_cast<float>(overlayW) : 0.0f;
|
|
||||||
float texH = overlayH > 0 ? static_cast<float>(overlayH) : 0.0f;
|
|
||||||
if (texW <= 0.0f || texH <= 0.0f) {
|
|
||||||
if (!SDL_GetTextureSize(overlayTex, &texW, &texH)) {
|
|
||||||
FILE* f = fopen("tetris_trace.log", "a");
|
|
||||||
if (f) {
|
|
||||||
fprintf(f, "MenuState::render failed to query overlay size: %s\n", SDL_GetError());
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
texW = 0.0f;
|
|
||||||
texH = 0.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (texW > 0.0f && texH > 0.0f) {
|
|
||||||
const float drawH = LOGICAL_H;
|
|
||||||
const float scale = drawH / texH;
|
|
||||||
const float drawW = texW * scale;
|
|
||||||
SDL_FRect dst{
|
|
||||||
(LOGICAL_W - drawW) * 0.5f + contentOffsetX,
|
|
||||||
contentOffsetY,
|
|
||||||
drawW,
|
|
||||||
drawH
|
|
||||||
};
|
|
||||||
int renderResult = SDL_RenderTexture(renderer, overlayTex, nullptr, &dst);
|
|
||||||
if (renderResult < 0) {
|
|
||||||
FILE* f = fopen("tetris_trace.log", "a");
|
|
||||||
if (f) {
|
|
||||||
fprintf(f, "MenuState::render failed to draw overlay: %s\n", SDL_GetError());
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
FILE* f = fopen("tetris_trace.log", "a");
|
|
||||||
if (f) {
|
|
||||||
fprintf(f, "MenuState::render no overlay texture available\n");
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test)
|
// Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test)
|
||||||
// Use the contentW calculated at the top with content offsets
|
// Use the contentW calculated at the top with content offsets
|
||||||
|
float contentW = LOGICAL_W * logicalScale;
|
||||||
bool isSmall = (contentW < 700.0f);
|
bool isSmall = (contentW < 700.0f);
|
||||||
float btnW = isSmall ? (LOGICAL_W * 0.32f) : (LOGICAL_W * 0.18f);
|
// Adjust button dimensions to match the background button graphics
|
||||||
btnW = std::clamp(btnW, 180.0f, 260.0f); // keep buttons from consuming entire row
|
float btnW = 200.0f; // Fixed width to match background buttons
|
||||||
float btnH = isSmall ? 56.0f : 64.0f;
|
float btnH = 70.0f; // Fixed height to match background buttons
|
||||||
float btnX = LOGICAL_W * 0.5f + contentOffsetX;
|
float btnX = LOGICAL_W * 0.5f + contentOffsetX;
|
||||||
// Move buttons down by 40px to match original layout (user requested 30-50px)
|
// Adjust vertical position to align with background buttons
|
||||||
const float btnYOffset = 40.0f;
|
float btnY = LOGICAL_H * 0.865f + contentOffsetY;
|
||||||
float btnY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; // align with main's button vertical position
|
|
||||||
|
|
||||||
if (ctx.pixelFont) {
|
if (ctx.pixelFont) {
|
||||||
{
|
{
|
||||||
@ -359,26 +280,6 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
|||||||
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
|
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
|
||||||
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
||||||
|
|
||||||
auto drawMenuButtonLocal = [&](SDL_Renderer* r, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, SDL_Color bg, SDL_Color border, bool selected){
|
|
||||||
float x = cx - w/2; float y = cy - h/2;
|
|
||||||
if (selected) {
|
|
||||||
SDL_SetRenderDrawColor(r, 255, 220, 0, 110);
|
|
||||||
SDL_FRect glow{ x-10, y-10, w+20, h+20 };
|
|
||||||
SDL_RenderFillRect(r, &glow);
|
|
||||||
}
|
|
||||||
SDL_SetRenderDrawColor(r, border.r, border.g, border.b, border.a);
|
|
||||||
SDL_FRect br{ x-6, y-6, w+12, h+12 }; SDL_RenderFillRect(r, &br);
|
|
||||||
SDL_SetRenderDrawColor(r, 255,255,255,255); SDL_FRect br2{ x-4, y-4, w+8, h+8 }; SDL_RenderFillRect(r, &br2);
|
|
||||||
SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a); SDL_FRect br3{ x, y, w, h }; SDL_RenderFillRect(r, &br3);
|
|
||||||
float textScale = 1.5f;
|
|
||||||
int textW = 0, textH = 0;
|
|
||||||
font.measure(label, textScale, textW, textH);
|
|
||||||
float tx = x + (w - static_cast<float>(textW)) * 0.5f;
|
|
||||||
float ty = y + (h - static_cast<float>(textH)) * 0.5f;
|
|
||||||
font.draw(r, tx + 2.0f, ty + 2.0f, label, textScale, SDL_Color{0, 0, 0, 200});
|
|
||||||
font.draw(r, tx, ty, label, textScale, SDL_Color{255, 255, 255, 255});
|
|
||||||
};
|
|
||||||
|
|
||||||
struct MenuButtonDef {
|
struct MenuButtonDef {
|
||||||
SDL_Color bg;
|
SDL_Color bg;
|
||||||
SDL_Color border;
|
SDL_Color border;
|
||||||
@ -392,136 +293,103 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
|||||||
MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" }
|
MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Icon array (nullptr if icon not loaded)
|
||||||
|
std::array<SDL_Texture*, 4> icons = {
|
||||||
|
playIcon,
|
||||||
|
levelIcon,
|
||||||
|
optionsIcon,
|
||||||
|
exitIcon
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fixed spacing to match background button positions
|
||||||
float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f;
|
float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f;
|
||||||
for (size_t i = 0; i < buttons.size(); ++i) {
|
|
||||||
|
// Draw each button individually so each can have its own coordinates
|
||||||
|
// Button 0 - PLAY
|
||||||
|
{
|
||||||
|
const int i = 0;
|
||||||
|
float cxCenter = 0.0f;
|
||||||
|
float cyCenter = btnY;
|
||||||
|
if (ctx.menuButtonsExplicit) {
|
||||||
|
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
|
||||||
|
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
|
||||||
|
} else {
|
||||||
float offset = (static_cast<float>(i) - 1.5f) * spacing;
|
float offset = (static_cast<float>(i) - 1.5f) * spacing;
|
||||||
float cx = btnX + offset;
|
cxCenter = btnX + offset;
|
||||||
drawMenuButtonLocal(renderer, *ctx.pixelFont, cx, btnY, btnW, btnH, buttons[i].label, buttons[i].bg, buttons[i].border, selectedButton == static_cast<int>(i));
|
}
|
||||||
|
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
|
||||||
|
buttons[i].label, false, selectedButton == i,
|
||||||
|
buttons[i].bg, buttons[i].border, true, icons[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button 1 - LEVEL
|
||||||
|
{
|
||||||
|
const int i = 1;
|
||||||
|
float cxCenter = 0.0f;
|
||||||
|
float cyCenter = btnY;
|
||||||
|
if (ctx.menuButtonsExplicit) {
|
||||||
|
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
|
||||||
|
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
|
||||||
|
} else {
|
||||||
|
float offset = (static_cast<float>(i) - 1.5f) * spacing;
|
||||||
|
cxCenter = btnX + offset;
|
||||||
|
}
|
||||||
|
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
|
||||||
|
buttons[i].label, false, selectedButton == i,
|
||||||
|
buttons[i].bg, buttons[i].border, true, icons[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button 2 - OPTIONS
|
||||||
|
{
|
||||||
|
const int i = 2;
|
||||||
|
float cxCenter = 0.0f;
|
||||||
|
float cyCenter = btnY;
|
||||||
|
if (ctx.menuButtonsExplicit) {
|
||||||
|
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
|
||||||
|
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
|
||||||
|
} else {
|
||||||
|
float offset = (static_cast<float>(i) - 1.5f) * spacing;
|
||||||
|
cxCenter = btnX + offset;
|
||||||
|
}
|
||||||
|
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
|
||||||
|
buttons[i].label, false, selectedButton == i,
|
||||||
|
buttons[i].bg, buttons[i].border, true, icons[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button 3 - EXIT
|
||||||
|
{
|
||||||
|
const int i = 3;
|
||||||
|
float cxCenter = 0.0f;
|
||||||
|
float cyCenter = btnY;
|
||||||
|
if (ctx.menuButtonsExplicit) {
|
||||||
|
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
|
||||||
|
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
|
||||||
|
} else {
|
||||||
|
float offset = (static_cast<float>(i) - 1.5f) * spacing;
|
||||||
|
cxCenter = btnX + offset;
|
||||||
|
}
|
||||||
|
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
|
||||||
|
buttons[i].label, false, selectedButton == i,
|
||||||
|
buttons[i].bg, buttons[i].border, true, icons[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) {
|
if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) {
|
||||||
int selection = ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
|
GameRenderer::renderExitPopup(
|
||||||
|
renderer,
|
||||||
// Switch to window coordinates for full-screen overlay
|
ctx.pixelFont,
|
||||||
SDL_SetRenderViewport(renderer, nullptr);
|
winW,
|
||||||
SDL_SetRenderScale(renderer, 1.0f, 1.0f);
|
winH,
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
logicalScale,
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 150);
|
ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1
|
||||||
|
);
|
||||||
// Get actual window size
|
|
||||||
int actualWinW = 0, actualWinH = 0;
|
|
||||||
SDL_GetRenderOutputSize(renderer, &actualWinW, &actualWinH);
|
|
||||||
SDL_FRect overlay{0, 0, (float)actualWinW, (float)actualWinH};
|
|
||||||
SDL_RenderFillRect(renderer, &overlay);
|
|
||||||
|
|
||||||
// Restore viewport and scale for popup content
|
|
||||||
SDL_SetRenderViewport(renderer, &logicalVP);
|
|
||||||
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
|
|
||||||
|
|
||||||
const float panelW = 640.0f;
|
|
||||||
const float panelH = 320.0f;
|
|
||||||
SDL_FRect panel{
|
|
||||||
(LOGICAL_W - panelW) * 0.5f + contentOffsetX,
|
|
||||||
(LOGICAL_H - panelH) * 0.5f + contentOffsetY,
|
|
||||||
panelW,
|
|
||||||
panelH
|
|
||||||
};
|
|
||||||
|
|
||||||
SDL_FRect shadow{panel.x + 6.0f, panel.y + 10.0f, panel.w, panel.h};
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140);
|
|
||||||
SDL_RenderFillRect(renderer, &shadow);
|
|
||||||
for (int i = 0; i < 5; ++i) {
|
|
||||||
SDL_FRect glow{panel.x - float(i * 2), panel.y - float(i * 2), panel.w + float(i * 4), panel.h + float(i * 4)};
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 180, 255, Uint8(44 - i * 7));
|
|
||||||
SDL_RenderRect(renderer, &glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_SetRenderDrawColor(renderer, 18, 30, 52, 255);
|
|
||||||
SDL_RenderFillRect(renderer, &panel);
|
|
||||||
SDL_SetRenderDrawColor(renderer, 70, 120, 210, 255);
|
|
||||||
SDL_RenderRect(renderer, &panel);
|
|
||||||
|
|
||||||
SDL_FRect inner{panel.x + 24.0f, panel.y + 98.0f, panel.w - 48.0f, panel.h - 146.0f};
|
|
||||||
SDL_SetRenderDrawColor(renderer, 16, 24, 40, 235);
|
|
||||||
SDL_RenderFillRect(renderer, &inner);
|
|
||||||
SDL_SetRenderDrawColor(renderer, 40, 80, 140, 235);
|
|
||||||
SDL_RenderRect(renderer, &inner);
|
|
||||||
|
|
||||||
FontAtlas* retroFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
|
|
||||||
if (retroFont) {
|
|
||||||
const float titleScale = 1.9f;
|
|
||||||
const char* title = "EXIT GAME?";
|
|
||||||
int titleW = 0, titleH = 0;
|
|
||||||
retroFont->measure(title, titleScale, titleW, titleH);
|
|
||||||
float titleX = panel.x + (panel.w - static_cast<float>(titleW)) * 0.5f;
|
|
||||||
retroFont->draw(renderer, titleX, panel.y + 30.0f, title, titleScale, SDL_Color{255, 230, 140, 255});
|
|
||||||
|
|
||||||
const float bodyScale = 1.05f;
|
|
||||||
const char* line = "Are you sure you want to quit?";
|
|
||||||
int bodyW = 0, bodyH = 0;
|
|
||||||
retroFont->measure(line, bodyScale, bodyW, bodyH);
|
|
||||||
float bodyX = panel.x + (panel.w - static_cast<float>(bodyW)) * 0.5f;
|
|
||||||
retroFont->draw(renderer, bodyX, inner.y + 18.0f, line, bodyScale, SDL_Color{210, 220, 240, 255});
|
|
||||||
}
|
|
||||||
|
|
||||||
const float horizontalPad = 28.0f;
|
|
||||||
const float buttonGap = 32.0f;
|
|
||||||
const float buttonH = 66.0f;
|
|
||||||
float buttonW = (inner.w - horizontalPad * 2.0f - buttonGap) * 0.5f;
|
|
||||||
float buttonY = inner.y + inner.h - buttonH - 24.0f;
|
|
||||||
|
|
||||||
auto drawChoice = [&](int idx, float x, const char* label) {
|
|
||||||
bool selected = (selection == idx);
|
|
||||||
SDL_Color base = (idx == 0) ? SDL_Color{185, 70, 70, 255} : SDL_Color{60, 95, 150, 255};
|
|
||||||
SDL_Color body = selected ? SDL_Color{Uint8(std::min(255, base.r + 35)), Uint8(std::min(255, base.g + 35)), Uint8(std::min(255, base.b + 35)), 255} : base;
|
|
||||||
SDL_Color border = selected ? SDL_Color{255, 220, 120, 255} : SDL_Color{80, 110, 160, 255};
|
|
||||||
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120);
|
|
||||||
SDL_FRect shadowRect{x + 4.0f, buttonY + 6.0f, buttonW, buttonH};
|
|
||||||
SDL_RenderFillRect(renderer, &shadowRect);
|
|
||||||
|
|
||||||
SDL_FRect bodyRect{x, buttonY, buttonW, buttonH};
|
|
||||||
SDL_SetRenderDrawColor(renderer, body.r, body.g, body.b, body.a);
|
|
||||||
SDL_RenderFillRect(renderer, &bodyRect);
|
|
||||||
SDL_SetRenderDrawColor(renderer, border.r, border.g, border.b, border.a);
|
|
||||||
SDL_RenderRect(renderer, &bodyRect);
|
|
||||||
|
|
||||||
if (retroFont) {
|
|
||||||
const float labelScale = 1.4f;
|
|
||||||
int textW = 0, textH = 0;
|
|
||||||
retroFont->measure(label, labelScale, textW, textH);
|
|
||||||
float textX = bodyRect.x + (bodyRect.w - static_cast<float>(textW)) * 0.5f;
|
|
||||||
float textY = bodyRect.y + (bodyRect.h - static_cast<float>(textH)) * 0.5f;
|
|
||||||
SDL_Color textColor = selected ? SDL_Color{255, 255, 255, 255} : SDL_Color{230, 235, 250, 255};
|
|
||||||
retroFont->draw(renderer, textX, textY, label, labelScale, textColor);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
float yesX = inner.x + horizontalPad;
|
|
||||||
float noX = yesX + buttonW + buttonGap;
|
|
||||||
drawChoice(0, yesX, "YES");
|
|
||||||
drawChoice(1, noX, "NO");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Popups (settings only - level popup is now a separate state)
|
// Popups (settings only - level popup is now a separate state)
|
||||||
if (ctx.showSettingsPopup && *ctx.showSettingsPopup) {
|
if (ctx.showSettingsPopup && *ctx.showSettingsPopup) {
|
||||||
// draw settings popup inline
|
|
||||||
bool musicOn = ctx.musicEnabled ? *ctx.musicEnabled : true;
|
bool musicOn = ctx.musicEnabled ? *ctx.musicEnabled : true;
|
||||||
float popupW = 350, popupH = 260;
|
bool soundOn = SoundEffectManager::instance().isEnabled();
|
||||||
float popupX = (LOGICAL_W - popupW) / 2;
|
UIRenderer::drawSettingsPopup(renderer, ctx.font, LOGICAL_W, LOGICAL_H, musicOn, soundOn);
|
||||||
float popupY = (LOGICAL_H - popupH) / 2;
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0,0,0,128); SDL_FRect overlay{0,0,(float)LOGICAL_W,(float)LOGICAL_H}; SDL_RenderFillRect(renderer, &overlay);
|
|
||||||
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);
|
|
||||||
ctx.font->draw(renderer, popupX + 20, popupY + 20, "SETTINGS", 2.0f, SDL_Color{255,220,0,255});
|
|
||||||
ctx.font->draw(renderer, popupX + 20, popupY + 70, "MUSIC:", 1.5f, SDL_Color{255,255,255,255});
|
|
||||||
ctx.font->draw(renderer, popupX + 120, popupY + 70, musicOn ? "ON" : "OFF", 1.5f, musicOn ? SDL_Color{0,255,0,255} : SDL_Color{255,0,0,255});
|
|
||||||
ctx.font->draw(renderer, popupX + 20, popupY + 100, "SOUND FX:", 1.5f, SDL_Color{255,255,255,255});
|
|
||||||
ctx.font->draw(renderer, popupX + 140, popupY + 100, "ON", 1.5f, SDL_Color{0,255,0,255});
|
|
||||||
ctx.font->draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, SDL_Color{200,200,220,255});
|
|
||||||
ctx.font->draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, SDL_Color{200,200,220,255});
|
|
||||||
ctx.font->draw(renderer, popupX + 20, popupY + 190, "ESC = CLOSE", 1.0f, SDL_Color{200,200,220,255});
|
|
||||||
}
|
}
|
||||||
// Trace exit
|
// Trace exit
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,4 +13,10 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = EXIT
|
int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = EXIT
|
||||||
|
|
||||||
|
// Button icons (optional - will use text if nullptr)
|
||||||
|
SDL_Texture* playIcon = nullptr;
|
||||||
|
SDL_Texture* levelIcon = nullptr;
|
||||||
|
SDL_Texture* optionsIcon = nullptr;
|
||||||
|
SDL_Texture* exitIcon = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include "../core/Settings.h"
|
#include "../core/Settings.h"
|
||||||
|
#include "../graphics/renderers/UIRenderer.h"
|
||||||
|
|
||||||
OptionsState::OptionsState(StateContext& ctx) : State(ctx) {}
|
OptionsState::OptionsState(StateContext& ctx) : State(ctx) {}
|
||||||
|
|
||||||
@ -89,32 +90,17 @@ void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
|||||||
|
|
||||||
float winW = static_cast<float>(logicalVP.w);
|
float winW = static_cast<float>(logicalVP.w);
|
||||||
float winH = static_cast<float>(logicalVP.h);
|
float winH = static_cast<float>(logicalVP.h);
|
||||||
float contentW = LOGICAL_W * logicalScale;
|
float contentOffsetX = 0.0f;
|
||||||
float contentH = LOGICAL_H * logicalScale;
|
float contentOffsetY = 0.0f;
|
||||||
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY);
|
||||||
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
|
||||||
|
|
||||||
SDL_Texture* logoTexture = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex;
|
SDL_Texture* logoTexture = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex;
|
||||||
if (logoTexture) {
|
int logoW = 0, logoH = 0;
|
||||||
float texW = 0.0f;
|
if (logoTexture == ctx.logoSmallTex && ctx.logoSmallW > 0) {
|
||||||
float texH = 0.0f;
|
logoW = ctx.logoSmallW;
|
||||||
if (logoTexture == ctx.logoSmallTex && ctx.logoSmallW > 0 && ctx.logoSmallH > 0) {
|
logoH = ctx.logoSmallH;
|
||||||
texW = static_cast<float>(ctx.logoSmallW);
|
|
||||||
texH = static_cast<float>(ctx.logoSmallH);
|
|
||||||
} else {
|
|
||||||
SDL_GetTextureSize(logoTexture, &texW, &texH);
|
|
||||||
}
|
|
||||||
if (texW > 0.0f && texH > 0.0f) {
|
|
||||||
float maxWidth = LOGICAL_W * 0.6f;
|
|
||||||
float scale = std::min(1.0f, maxWidth / texW);
|
|
||||||
float dw = texW * scale;
|
|
||||||
float dh = texH * scale;
|
|
||||||
float logoX = (LOGICAL_W - dw) * 0.5f + contentOffsetX;
|
|
||||||
float logoY = LOGICAL_H * 0.05f + contentOffsetY;
|
|
||||||
SDL_FRect dst{logoX, logoY, dw, dh};
|
|
||||||
SDL_RenderTexture(renderer, logoTexture, nullptr, &dst);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
UIRenderer::drawLogo(renderer, logoTexture, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY, logoW, logoH);
|
||||||
|
|
||||||
const float panelW = 520.0f;
|
const float panelW = 520.0f;
|
||||||
const float panelH = 420.0f;
|
const float panelH = 420.0f;
|
||||||
@ -125,23 +111,7 @@ void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
|||||||
panelH
|
panelH
|
||||||
};
|
};
|
||||||
|
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
UIRenderer::drawSciFiPanel(renderer, panel);
|
||||||
|
|
||||||
// Panel styling similar to level selector
|
|
||||||
SDL_FRect shadow{panel.x + 6.0f, panel.y + 10.0f, panel.w, panel.h};
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120);
|
|
||||||
SDL_RenderFillRect(renderer, &shadow);
|
|
||||||
|
|
||||||
for (int i = 0; i < 5; ++i) {
|
|
||||||
SDL_FRect glow{panel.x - float(i * 2), panel.y - float(i * 2), panel.w + float(i * 4), panel.h + float(i * 4)};
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 180, 255, Uint8(42 - i * 8));
|
|
||||||
SDL_RenderRect(renderer, &glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_SetRenderDrawColor(renderer, 18, 30, 52, 255);
|
|
||||||
SDL_RenderFillRect(renderer, &panel);
|
|
||||||
SDL_SetRenderDrawColor(renderer, 70, 120, 210, 255);
|
|
||||||
SDL_RenderRect(renderer, &panel);
|
|
||||||
|
|
||||||
FontAtlas* retroFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
|
FontAtlas* retroFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
// Forward declarations for frequently used types
|
// Forward declarations for frequently used types
|
||||||
class Game;
|
class Game;
|
||||||
@ -66,6 +67,12 @@ struct StateContext {
|
|||||||
std::function<void(AppState)> requestFadeTransition; // Generic state fade requests (menu/options/level)
|
std::function<void(AppState)> requestFadeTransition; // Generic state fade requests (menu/options/level)
|
||||||
// Pointer to the application's StateManager so states can request transitions
|
// Pointer to the application's StateManager so states can request transitions
|
||||||
StateManager* stateManager = nullptr;
|
StateManager* stateManager = nullptr;
|
||||||
|
// Optional explicit per-button coordinates (logical coordinates). When
|
||||||
|
// `menuButtonsExplicit` is true, MenuState will use these centers for
|
||||||
|
// rendering and hit tests. Values are in logical units (LOGICAL_W/H).
|
||||||
|
std::array<float, 4> menuButtonCX{};
|
||||||
|
std::array<float, 4> menuButtonCY{};
|
||||||
|
bool menuButtonsExplicit = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class State {
|
class State {
|
||||||
|
|||||||
Reference in New Issue
Block a user